Compare commits

..

4 Commits

Author SHA1 Message Date
John McCardle 43fac8f4f3 Typo in UIFrame repr 2024-04-20 18:32:52 -04:00
John McCardle 3fd5ad93e2 Add UIGridPoint and UIGridPointState repr 2024-04-20 18:32:30 -04:00
John McCardle 03376897b8 Add UIGrid repr 2024-04-20 18:32:17 -04:00
John McCardle 48af072a33 Add UIEntity repr 2024-04-20 18:32:05 -04:00
354 changed files with 2972 additions and 1795344 deletions

23
.gitignore vendored
View File

@ -8,26 +8,5 @@ PCbuild
obj obj
build build
lib lib
__pycache__ obj
.cache/
7DRL2025 Release/
CMakeFiles/
Makefile
*.zip
__lib/
_oldscripts/
assets/
cellular_automata_fire/
deps/
fetch_issues_txt.py
forest_fire_CA.py
mcrogueface.github.io
scripts/
tcod_reference
.archive
# Keep important documentation and tests
!CLAUDE.md
!README.md
!tests/

7
.gitmodules vendored
View File

@ -10,7 +10,6 @@
[submodule "modules/SFML"] [submodule "modules/SFML"]
path = modules/SFML path = modules/SFML
url = git@github.com:SFML/SFML.git url = git@github.com:SFML/SFML.git
[submodule "modules/libtcod-headless"] [submodule "modules/libtcod"]
path = modules/libtcod-headless path = modules/libtcod
url = git@github.com:jmccardle/libtcod-headless.git url = git@github.com:libtcod/libtcod.git
branch = 2.2.1-headless

View File

@ -1,9 +0,0 @@
{
"mcpServers": {
"gitea": {
"type": "stdio",
"command": "/home/john/Development/discord_for_claude/forgejo-mcp.linux.amd64",
"args": ["stdio", "--server", "https://gamedev.ffwf.net/gitea/", "--token", "f58ec698a5edee82db4960920b13d3f7d0d58d8e"]
}
}
}

View File

@ -1,306 +0,0 @@
# Building McRogueFace from Source
This document describes how to build McRogueFace from a fresh clone.
## Build Options
There are two ways to build McRogueFace:
1. **Quick Build** (recommended): Use pre-built dependency libraries from a `build_deps` archive
2. **Full Build**: Compile all dependencies from submodules
## Prerequisites
### System Dependencies
Install these packages before building:
```bash
# Debian/Ubuntu
sudo apt install \
build-essential \
cmake \
git \
zlib1g-dev \
libx11-dev \
libxrandr-dev \
libxcursor-dev \
libfreetype-dev \
libudev-dev \
libvorbis-dev \
libflac-dev \
libgl-dev \
libopenal-dev
```
**Note:** SDL is NOT required - McRogueFace uses libtcod-headless which has no SDL dependency.
---
## Option 1: Quick Build (Using Pre-built Dependencies)
If you have a `build_deps.tar.gz` or `build_deps.zip` archive:
```bash
# Clone McRogueFace (no submodules needed)
git clone <repository-url> McRogueFace
cd McRogueFace
# Extract pre-built dependencies
tar -xzf /path/to/build_deps.tar.gz
# Or for zip: unzip /path/to/build_deps.zip
# Build McRogueFace
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
# Run
./mcrogueface
```
The `build_deps` archive contains:
- `__lib/` - Pre-built shared libraries (Python, SFML, libtcod-headless)
- `deps/` - Header symlinks for compilation
**Total build time: ~30 seconds**
---
## Option 2: Full Build (Compiling All Dependencies)
### 1. Clone with Submodules
```bash
git clone --recursive <repository-url> McRogueFace
cd McRogueFace
```
If submodules weren't cloned:
```bash
git submodule update --init --recursive
```
**Note:** imgui/imgui-sfml submodules may fail - this is fine, they're not used.
### 2. Create Dependency Symlinks
```bash
cd deps
ln -sf ../modules/cpython cpython
ln -sf ../modules/libtcod-headless/src/libtcod libtcod
ln -sf ../modules/cpython/Include Python
ln -sf ../modules/SFML/include/SFML SFML
cd ..
```
### 3. Build libtcod-headless
libtcod-headless is our SDL-free fork with vendored dependencies:
```bash
cd modules/libtcod-headless
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=ON
make -j$(nproc)
cd ../../..
```
That's it! No special flags needed - libtcod-headless defaults to:
- `LIBTCOD_SDL3=disable` (no SDL dependency)
- Vendored lodepng, utf8proc, stb
### 4. Build Python 3.12
```bash
cd modules/cpython
./configure --enable-shared
make -j$(nproc)
cd ../..
```
### 5. Build SFML 2.6
```bash
cd modules/SFML
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=ON
make -j$(nproc)
cd ../../..
```
### 6. Copy Libraries
```bash
mkdir -p __lib
# Python
cp modules/cpython/libpython3.12.so* __lib/
# SFML
cp modules/SFML/build/lib/libsfml-*.so* __lib/
# libtcod-headless
cp modules/libtcod-headless/build/bin/libtcod.so* __lib/
# Python standard library
cp -r modules/cpython/Lib __lib/Python
```
### 7. Build McRogueFace
```bash
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
```
### 8. Run
```bash
./mcrogueface
```
---
## Submodule Versions
| Submodule | Version | Notes |
|-----------|---------|-------|
| SFML | 2.6.1 | Graphics, audio, windowing |
| cpython | 3.12.2 | Embedded Python interpreter |
| libtcod-headless | 2.2.1 | SDL-free fork for FOV, pathfinding |
---
## Creating a build_deps Archive
To create a `build_deps` archive for distribution:
```bash
cd McRogueFace
# Create archive directory
mkdir -p build_deps_staging
# Copy libraries
cp -r __lib build_deps_staging/
# Copy/create deps symlinks as actual directories with only needed headers
mkdir -p build_deps_staging/deps
cp -rL deps/libtcod build_deps_staging/deps/ # Follow symlink
cp -rL deps/Python build_deps_staging/deps/
cp -rL deps/SFML build_deps_staging/deps/
cp -r deps/platform build_deps_staging/deps/
# Create archives
cd build_deps_staging
tar -czf ../build_deps.tar.gz __lib deps
zip -r ../build_deps.zip __lib deps
cd ..
# Cleanup
rm -rf build_deps_staging
```
The resulting archive can be distributed alongside releases for users who want to build McRogueFace without compiling dependencies.
**Archive contents:**
```
build_deps.tar.gz
├── __lib/
│ ├── libpython3.12.so*
│ ├── libsfml-*.so*
│ ├── libtcod.so*
│ └── Python/ # Python standard library
└── deps/
├── libtcod/ # libtcod headers
├── Python/ # Python headers
├── SFML/ # SFML headers
└── platform/ # Platform-specific configs
```
---
## Verify the Build
```bash
cd build
# Check version
./mcrogueface --version
# Test headless mode
./mcrogueface --headless -c "import mcrfpy; print('Success')"
# Verify no SDL dependencies
ldd mcrogueface | grep -i sdl # Should output nothing
```
---
## Troubleshooting
### OpenAL not found
```bash
sudo apt install libopenal-dev
```
### FreeType not found
```bash
sudo apt install libfreetype-dev
```
### X11/Xrandr not found
```bash
sudo apt install libx11-dev libxrandr-dev
```
### Python standard library missing
Ensure `__lib/Python` contains the standard library:
```bash
ls __lib/Python/os.py # Should exist
```
### libtcod symbols not found
Ensure libtcod.so is in `__lib/` with correct version:
```bash
ls -la __lib/libtcod.so*
# Should show libtcod.so -> libtcod.so.2 -> libtcod.so.2.2.1
```
---
## Build Times (approximate)
On a typical 4-core system:
| Component | Time |
|-----------|------|
| libtcod-headless | ~30 seconds |
| Python 3.12 | ~3-5 minutes |
| SFML 2.6 | ~1 minute |
| McRogueFace | ~30 seconds |
| **Full build total** | **~5-7 minutes** |
| **Quick build (pre-built deps)** | **~30 seconds** |
---
## Runtime Dependencies
The built executable requires these system libraries:
- `libz.so.1` (zlib)
- `libopenal.so.1` (OpenAL)
- `libX11.so.6`, `libXrandr.so.2` (X11)
- `libfreetype.so.6` (FreeType)
- `libGL.so.1` (OpenGL)
All other dependencies (Python, SFML, libtcod) are bundled in `lib/`.

626
CLAUDE.md
View File

@ -1,626 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Gitea-First Workflow
**IMPORTANT**: This project uses Gitea for issue tracking, documentation, and project management. Always consult and update Gitea resources before and during development work.
**Gitea Instance**: https://gamedev.ffwf.net/gitea/john/McRogueFace
### Core Principles
1. **Gitea is the Single Source of Truth**
- Issue tracker contains current tasks, bugs, and feature requests
- Wiki contains living documentation and architecture decisions
- Use Gitea MCP tools to query and update issues programmatically
2. **Always Check Gitea First**
- Before starting work: Check open issues for related tasks or blockers
- When using `/roadmap` command: Query Gitea for up-to-date issue status
- When researching a feature: Search Gitea wiki and issues before grepping codebase
- When encountering a bug: Check if an issue already exists
3. **Create Granular Issues**
- Break large features into separate, focused issues
- Each issue should address one specific problem or enhancement
- Tag issues appropriately: `[Bugfix]`, `[Major Feature]`, `[Minor Feature]`, etc.
- Link related issues using dependencies or blocking relationships
4. **Document as You Go**
- When work on one issue interacts with another system: Add notes to related issues
- When discovering undocumented behavior: Create task to document it
- When documentation misleads you: Create task to correct or expand it
- When implementing a feature: Update the Gitea wiki if appropriate
5. **Cross-Reference Everything**
- Commit messages should reference issue numbers (e.g., "Fixes #104", "Addresses #125")
- Issue comments should link to commits when work is done
- Wiki pages should reference relevant issues for implementation details
- Issues should link to each other when dependencies exist
### Workflow Pattern
```
┌─────────────────────────────────────────────────────┐
│ 1. Check Gitea Issues & Wiki │
│ - Is there an existing issue for this? │
│ - What's the current status? │
│ - Are there related issues or blockers? │
└─────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 2. Create Issues (if needed) │
│ - Break work into granular tasks │
│ - Tag appropriately │
│ - Link dependencies │
└─────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 3. Do the Work │
│ - Implement/fix/document │
│ - Write tests first (TDD) │
│ - Add inline documentation │
└─────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 4. Update Gitea │
│ - Add notes to affected issues │
│ - Create follow-up issues for discovered work │
│ - Update wiki if architecture/APIs changed │
│ - Add documentation correction tasks │
└─────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 5. Commit & Reference │
│ - Commit messages reference issue numbers │
│ - Close issues or update status │
│ - Add commit links to issue comments │
└─────────────────────────────────────────────────────┘
```
### Benefits of Gitea-First Approach
- **Reduced Context Switching**: Check brief issue descriptions instead of re-reading entire codebase
- **Better Planning**: Issues provide roadmap; avoid duplicate or contradictory work
- **Living Documentation**: Wiki and issues stay current as work progresses
- **Historical Context**: Issue comments capture why decisions were made
- **Efficiency**: MCP tools allow programmatic access to project state
### MCP Tools Available
Claude Code has access to Gitea MCP tools for:
- `list_repo_issues` - Query current issues with filtering
- `get_issue` - Get detailed issue information
- `create_issue` - Create new issues programmatically
- `create_issue_comment` - Add comments to issues
- `edit_issue` - Update issue status, title, body
- `add_issue_labels` - Tag issues appropriately
- `add_issue_dependency` / `add_issue_blocking` - Link related issues
- Plus wiki, milestone, and label management tools
Use these tools liberally to keep the project organized!
### Gitea Label System
**IMPORTANT**: Always apply appropriate labels when creating new issues!
The project uses a structured label system to organize issues:
**Label Categories:**
1. **System Labels** (identify affected codebase area):
- `system:rendering` - Rendering pipeline and visuals
- `system:ui-hierarchy` - UI component hierarchy and composition
- `system:grid` - Grid system and spatial containers
- `system:animation` - Animation and property interpolation
- `system:python-binding` - Python/C++ binding layer
- `system:input` - Input handling and events
- `system:performance` - Performance optimization and profiling
- `system:documentation` - Documentation infrastructure
2. **Priority Labels** (development timeline):
- `priority:tier1-active` - Current development focus - critical path to v1.0
- `priority:tier2-foundation` - Important foundation work - not blocking v1.0
- `priority:tier3-future` - Future features - deferred until after v1.0
3. **Type/Scope Labels** (effort and complexity):
- `Major Feature` - Significant time and effort required
- `Minor Feature` - Some effort required to create or overhaul functionality
- `Tiny Feature` - Quick and easy - a few lines or little interconnection
- `Bugfix` - Fixes incorrect behavior
- `Refactoring & Cleanup` - No new functionality, just improving codebase
- `Documentation` - Documentation work
- `Demo Target` - Functionality to demonstrate
4. **Workflow Labels** (current blockers/needs):
- `workflow:blocked` - Blocked by other work - waiting on dependencies
- `workflow:needs-documentation` - Needs documentation before or after implementation
- `workflow:needs-benchmark` - Needs performance testing and benchmarks
- `Alpha Release Requirement` - Blocker to 0.1 Alpha release
**When creating issues:**
- Apply at least one `system:*` label (what part of codebase)
- Apply one `priority:tier*` label (when to address it)
- Apply one type label (`Major Feature`, `Minor Feature`, `Tiny Feature`, or `Bugfix`)
- Apply `workflow:*` labels if applicable (blocked, needs docs, needs benchmarks)
**Example label combinations:**
- New rendering feature: `system:rendering`, `priority:tier2-foundation`, `Major Feature`
- Python API improvement: `system:python-binding`, `priority:tier1-active`, `Minor Feature`
- Performance work: `system:performance`, `priority:tier1-active`, `Major Feature`, `workflow:needs-benchmark`
**⚠️ CRITICAL BUG**: The Gitea MCP tool (v0.07) has a label application bug documented in `GITEA_MCP_LABEL_BUG_REPORT.md`:
- `add_issue_labels` and `replace_issue_labels` behave inconsistently
- Single ID arrays produce different results than multi-ID arrays for the SAME IDs
- Label IDs do not map reliably to actual labels
**Workaround Options:**
1. **Best**: Apply labels manually via web interface: `https://gamedev.ffwf.net/gitea/john/McRogueFace/issues/<number>`
2. **Automated**: Apply labels ONE AT A TIME using single-element arrays (slower but more reliable)
3. **Use single-ID mapping** (documented below)
**Label ID Reference** (for documentation purposes - see issue #131 for details):
```
1=Major Feature, 2=Alpha Release, 3=Bugfix, 4=Demo Target, 5=Documentation,
6=Minor Feature, 7=tier1-active, 8=tier2-foundation, 9=tier3-future,
10=Refactoring, 11=animation, 12=docs, 13=grid, 14=input, 15=performance,
16=python-binding, 17=rendering, 18=ui-hierarchy, 19=Tiny Feature,
20=blocked, 21=needs-benchmark, 22=needs-documentation
```
## Build Commands
```bash
# Build the project (compiles to ./build directory)
make
# Or use the build script directly
./build.sh
# Run the game
make run
# Clean build artifacts
make clean
# The executable and all assets are in ./build/
cd build
./mcrogueface
```
## Project Architecture
McRogueFace is a C++ game engine with Python scripting support, designed for creating roguelike games. The architecture consists of:
### Core Engine (C++)
- **Entry Point**: `src/main.cpp` initializes the game engine
- **Scene System**: `Scene.h/cpp` manages game states
- **Entity System**: `UIEntity.h/cpp` provides game objects
- **Python Integration**: `McRFPy_API.h/cpp` exposes engine functionality to Python
- **UI Components**: `UIFrame`, `UICaption`, `UISprite`, `UIGrid` for rendering
### Game Logic (Python)
- **Main Script**: `src/scripts/game.py` contains game initialization and scene setup
- **Entity System**: `src/scripts/cos_entities.py` implements game entities (Player, Enemy, Boulder, etc.)
- **Level Generation**: `src/scripts/cos_level.py` uses BSP for procedural dungeon generation
- **Tile System**: `src/scripts/cos_tiles.py` implements Wave Function Collapse for tile placement
### Key Python API (`mcrfpy` module)
The C++ engine exposes these primary functions to Python:
- Scene Management: `createScene()`, `setScene()`, `sceneUI()`
- Entity Creation: `Entity()` with position and sprite properties
- Grid Management: `Grid()` for tilemap rendering
- Input Handling: `keypressScene()` for keyboard events
- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()`
- Timers: `setTimer()`, `delTimer()` for event scheduling
## Development Workflow
### Running the Game
After building, the executable expects:
- `assets/` directory with sprites, fonts, and audio
- `scripts/` directory with Python game files
- Python 3.12 shared libraries in `./lib/`
### Modifying Game Logic
- Game scripts are in `src/scripts/`
- Main game entry is `game.py`
- Entity behavior in `cos_entities.py`
- Level generation in `cos_level.py`
### Adding New Features
1. C++ API additions go in `src/McRFPy_API.cpp`
2. Expose to Python using the existing binding pattern
3. Update Python scripts to use new functionality
## Testing
### Test Suite Structure
The `tests/` directory contains the comprehensive test suite:
```
tests/
├── run_tests.py # Test runner - executes all tests with timeout
├── unit/ # Unit tests for individual components (105+ tests)
├── integration/ # Integration tests for system interactions
├── regression/ # Bug regression tests (issue_XX_*.py)
├── benchmarks/ # Performance benchmarks
├── demo/ # Feature demonstration system
│ ├── demo_main.py # Interactive demo runner
│ ├── screens/ # Per-feature demo screens
│ └── screenshots/ # Generated demo screenshots
└── notes/ # Analysis files and documentation
```
### Running Tests
```bash
# Run the full test suite (from tests/ directory)
cd tests && python3 run_tests.py
# Run a specific test
cd build && ./mcrogueface --headless --exec ../tests/unit/some_test.py
# Run the demo system interactively
cd build && ./mcrogueface ../tests/demo/demo_main.py
# Generate demo screenshots (headless)
cd build && ./mcrogueface --headless --exec ../tests/demo/demo_main.py
```
### Reading Tests as Examples
**IMPORTANT**: Before implementing a feature or fixing a bug, check existing tests for API usage examples:
- `tests/unit/` - Shows correct usage of individual mcrfpy classes and functions
- `tests/demo/screens/` - Complete working examples of UI components
- `tests/regression/` - Documents edge cases and bug scenarios
Example: To understand Animation API:
```bash
grep -r "Animation" tests/unit/
cat tests/demo/screens/animation_demo.py
```
### Writing Tests
**Always write tests when adding features or fixing bugs:**
1. **For new features**: Create `tests/unit/feature_name_test.py`
2. **For bug fixes**: Create `tests/regression/issue_XX_description_test.py`
3. **For demos**: Add to `tests/demo/screens/` if it showcases a feature
### Quick Testing Commands
```bash
# Test headless mode with inline Python
cd build
./mcrogueface --headless -c "import mcrfpy; print('Headless test')"
# Run specific test with output
./mcrogueface --headless --exec ../tests/unit/my_test.py 2>&1
```
## Common Development Tasks
### Compiling McRogueFace
```bash
# Standard build (to ./build directory)
make
# Full rebuild
make clean && make
# Manual CMake build
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
# The library path issue: if linking fails, check that libraries are in __lib/
# CMakeLists.txt expects: link_directories(${CMAKE_SOURCE_DIR}/__lib)
```
### Running and Capturing Output
```bash
# Run with timeout and capture output
cd build
timeout 5 ./mcrogueface 2>&1 | tee output.log
# Run in background and kill after delay
./mcrogueface > output.txt 2>&1 & PID=$!; sleep 3; kill $PID 2>/dev/null
# Just capture first N lines (useful for crashes)
./mcrogueface 2>&1 | head -50
```
### Debugging with GDB
```bash
# Interactive debugging
gdb ./mcrogueface
(gdb) run
(gdb) bt # backtrace after crash
# Batch mode debugging (non-interactive)
gdb -batch -ex run -ex where -ex quit ./mcrogueface 2>&1
# Get just the backtrace after a crash
gdb -batch -ex "run" -ex "bt" ./mcrogueface 2>&1 | head -50
# Debug with specific commands
echo -e "run\nbt 5\nquit\ny" | gdb ./mcrogueface 2>&1
```
### Testing Different Python Scripts
```bash
# The game automatically runs build/scripts/game.py on startup
# To test different behavior:
# Option 1: Replace game.py temporarily
cd build
cp scripts/my_test_script.py scripts/game.py
./mcrogueface
# Option 2: Backup original and test
mv scripts/game.py scripts/game.py.bak
cp my_test.py scripts/game.py
./mcrogueface
mv scripts/game.py.bak scripts/game.py
# Option 3: For quick tests, create minimal game.py
echo 'import mcrfpy; print("Test"); mcrfpy.createScene("test")' > scripts/game.py
```
### Understanding Key Macros and Patterns
#### RET_PY_INSTANCE Macro (UIDrawable.h)
This macro handles converting C++ UI objects to their Python equivalents:
```cpp
RET_PY_INSTANCE(target);
// Expands to a switch on target->derived_type() that:
// 1. Allocates the correct Python object type (Frame, Caption, Sprite, Grid)
// 2. Sets the shared_ptr data member
// 3. Returns the PyObject*
```
#### Collection Patterns
- `UICollection` wraps `std::vector<std::shared_ptr<UIDrawable>>`
- `UIEntityCollection` wraps `std::list<std::shared_ptr<UIEntity>>`
- Different containers require different iteration code (vector vs list)
#### Python Object Creation Patterns
```cpp
// Pattern 1: Using tp_alloc (most common)
auto o = (PyUIFrameObject*)type->tp_alloc(type, 0);
o->data = std::make_shared<UIFrame>();
// Pattern 2: Getting type from module
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
// Pattern 3: Direct shared_ptr assignment
iterObj->data = self->data; // Shares the C++ object
```
### Working Directory Structure
```
build/
├── mcrogueface # The executable
├── scripts/
│ └── game.py # Auto-loaded Python script
├── assets/ # Copied from source during build
└── lib/ # Python libraries (copied from __lib/)
```
### Quick Iteration Tips
- Keep a test script ready for quick experiments
- Use `timeout` to auto-kill hanging processes
- The game expects a window manager; use Xvfb for headless testing
- Python errors go to stderr, game output to stdout
- Segfaults usually mean Python type initialization issues
## Important Notes
- The project uses SFML for graphics/audio and libtcod for roguelike utilities
- Python scripts are loaded at runtime from the `scripts/` directory
- Asset loading expects specific paths relative to the executable
- The game was created for 7DRL 2025 as "Crypt of Sokoban"
- Iterator implementations require careful handling of C++/Python boundaries
## Testing Guidelines
### Test-Driven Development
- **Always write tests first**: Create tests in `./tests/` for all bugs and new features
- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix
- **Read existing tests**: Check `tests/unit/` and `tests/demo/screens/` for API examples before writing code
- **Close the loop**: Reproduce issue → change code → recompile → run test → verify
### Two Types of Tests
#### 1. Direct Execution Tests (No Game Loop)
For tests that only need class initialization or direct code execution:
```python
# tests/unit/my_feature_test.py
import mcrfpy
import sys
# Test code - runs immediately
frame = mcrfpy.Frame(pos=(0,0), size=(100,100))
assert frame.x == 0
assert frame.w == 100
print("PASS")
sys.exit(0)
```
#### 2. Game Loop Tests (Timer-Based)
For tests requiring rendering, screenshots, or elapsed time:
```python
# tests/unit/my_visual_test.py
import mcrfpy
from mcrfpy import automation
import sys
def run_test(runtime):
"""Timer callback - runs after game loop starts"""
automation.screenshot("test_result.png")
# Validate results...
print("PASS")
sys.exit(0)
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
ui.append(mcrfpy.Frame(pos=(50,50), size=(100,100)))
mcrfpy.setScene("test")
mcrfpy.setTimer("test", run_test, 100)
```
### Key Testing Principles
- **Timer callbacks are essential**: Screenshots only work after the render loop starts
- **Use automation API**: `automation.screenshot()`, `automation.click()` for visual testing
- **Exit properly**: Always call `sys.exit(0)` for PASS or `sys.exit(1)` for FAIL
- **Headless mode**: Use `--headless --exec` for CI/automated testing
- **Check examples first**: Read `tests/demo/screens/*.py` for correct API usage
### API Quick Reference (from tests)
```python
# Animation: (property, target_value, duration, easing)
anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut")
anim.start(frame)
# Caption: use keyword arguments to avoid positional conflicts
cap = mcrfpy.Caption(text="Hello", pos=(100, 100))
# Grid center: uses pixel coordinates, not cell coordinates
grid = mcrfpy.Grid(grid_size=(15, 10), pos=(50, 50), size=(400, 300))
grid.center = (120, 80) # pixels: (cells * cell_size / 2)
# Keyboard handler: key names are "Num1", "Num2", "Escape", "Q", etc.
def on_key(key, state):
if key == "Num1" and state == "start":
mcrfpy.setScene("demo_1")
```
## Development Best Practices
### Testing and Deployment
- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, tests shouldn't be included
- **Run full suite before commits**: `cd tests && python3 run_tests.py`
## Documentation Guidelines
### Documentation Macro System
**As of 2025-10-30, McRogueFace uses a macro-based documentation system** (`src/McRFPy_Doc.h`) that ensures consistent, complete docstrings across all Python bindings.
#### Include the Header
```cpp
#include "McRFPy_Doc.h"
```
#### Documenting Methods
For methods in PyMethodDef arrays, use `MCRF_METHOD`:
```cpp
{"method_name", (PyCFunction)Class::method, METH_VARARGS,
MCRF_METHOD(ClassName, method_name,
MCRF_SIG("(arg1: type, arg2: type)", "return_type"),
MCRF_DESC("Brief description of what the method does."),
MCRF_ARGS_START
MCRF_ARG("arg1", "Description of first argument")
MCRF_ARG("arg2", "Description of second argument")
MCRF_RETURNS("Description of return value")
MCRF_RAISES("ValueError", "Condition that raises this exception")
MCRF_NOTE("Important notes or caveats")
MCRF_LINK("docs/guide.md", "Related Documentation")
)},
```
#### Documenting Properties
For properties in PyGetSetDef arrays, use `MCRF_PROPERTY`:
```cpp
{"property_name", (getter)getter_func, (setter)setter_func,
MCRF_PROPERTY(property_name,
"Brief description of the property. "
"Additional details about valid values, side effects, etc."
), NULL},
```
#### Available Macros
- `MCRF_SIG(params, ret)` - Method signature
- `MCRF_DESC(text)` - Description paragraph
- `MCRF_ARGS_START` - Begin arguments section
- `MCRF_ARG(name, desc)` - Individual argument
- `MCRF_RETURNS(text)` - Return value description
- `MCRF_RAISES(exception, condition)` - Exception documentation
- `MCRF_NOTE(text)` - Important notes
- `MCRF_LINK(path, text)` - Reference to external documentation
#### Documentation Prose Guidelines
**Keep C++ docstrings concise** (1-2 sentences per section). For complex topics, use `MCRF_LINK` to reference external guides:
```cpp
MCRF_LINK("docs/animation-guide.md", "Animation System Tutorial")
```
**External documentation** (in `docs/`) can be verbose with examples, tutorials, and design rationale.
### Regenerating Documentation
After modifying C++ inline documentation with MCRF_* macros:
1. **Rebuild the project**: `make -j$(nproc)`
2. **Generate all documentation** (recommended - single command):
```bash
./tools/generate_all_docs.sh
```
This creates:
- `docs/api_reference_dynamic.html` - HTML API reference
- `docs/API_REFERENCE_DYNAMIC.md` - Markdown API reference
- `docs/mcrfpy.3` - Unix man page (section 3)
- `stubs/mcrfpy.pyi` - Type stubs for IDE support
3. **Or generate individually**:
```bash
# API docs (HTML + Markdown)
./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py
# Type stubs (manually-maintained with @overload support)
./build/mcrogueface --headless --exec tools/generate_stubs_v2.py
# Man page (requires pandoc)
./tools/generate_man_page.sh
```
**System Requirements:**
- `pandoc` must be installed for man page generation: `sudo apt-get install pandoc`
### Important Notes
- **Single source of truth**: Documentation lives in C++ source files via MCRF_* macros
- **McRogueFace as Python interpreter**: Documentation scripts MUST be run using McRogueFace itself, not system Python
- **Use --headless --exec**: For non-interactive documentation generation
- **Link transformation**: `MCRF_LINK` references are transformed to appropriate format (HTML, Markdown, etc.)
- **No manual dictionaries**: The old hardcoded documentation system has been removed
### Documentation Pipeline Architecture
1. **C++ Source** → MCRF_* macros in PyMethodDef/PyGetSetDef arrays
2. **Compilation** → Macros expand to complete docstrings embedded in module
3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract
4. **Generation** → HTML/Markdown/Stub files created with transformed links
5. **No drift**: Impossible for docs and code to disagree - they're the same file!
The macro system ensures complete, consistent documentation across all Python bindings.
- Close issues automatically in gitea by adding to the commit message "closes #X", where X is the issue number. This associates the issue closure with the specific commit, so granular commits are preferred. You should only use the MCP tool to close issues directly when discovering that the issue is already complete; when committing changes, always such "closes" (or the opposite, "reopens") references to related issues. If on a feature branch, the issue will be referenced by the commit, and when merged to master, the issue will be actually closed (or reopened).

View File

@ -17,69 +17,40 @@ include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython) include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
include_directories(${CMAKE_SOURCE_DIR}/deps/Python) include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
# ImGui and ImGui-SFML include directories
include_directories(${CMAKE_SOURCE_DIR}/modules/imgui)
include_directories(${CMAKE_SOURCE_DIR}/modules/imgui-sfml)
# ImGui source files
set(IMGUI_SOURCES
${CMAKE_SOURCE_DIR}/modules/imgui/imgui.cpp
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_draw.cpp
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_tables.cpp
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_widgets.cpp
${CMAKE_SOURCE_DIR}/modules/imgui-sfml/imgui-SFML.cpp
)
# Collect all the source files # Collect all the source files
file(GLOB_RECURSE SOURCES "src/*.cpp") file(GLOB_RECURSE SOURCES "src/*.cpp")
# Add ImGui sources to the build
list(APPEND SOURCES ${IMGUI_SOURCES})
# Find OpenGL (required by ImGui-SFML)
find_package(OpenGL REQUIRED)
# Create a list of libraries to link against # Create a list of libraries to link against
set(LINK_LIBS set(LINK_LIBS
m
dl
util
pthread
python3.12
sfml-graphics sfml-graphics
sfml-window sfml-window
sfml-system sfml-system
sfml-audio sfml-audio
tcod tcod)
OpenGL::GL)
# On Windows, add any additional libs and include directories # On Windows, add any additional libs and include directories
if(WIN32) if(WIN32)
# Windows-specific Python library name (no dots)
list(APPEND LINK_LIBS python314)
# Add the necessary Windows-specific libraries and include directories # Add the necessary Windows-specific libraries and include directories
# include_directories(path_to_additional_includes) # include_directories(path_to_additional_includes)
# link_directories(path_to_additional_libs) # link_directories(path_to_additional_libs)
# list(APPEND LINK_LIBS additional_windows_libs) # list(APPEND LINK_LIBS additional_windows_libs)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows) include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
else() else()
# Unix/Linux specific libraries
list(APPEND LINK_LIBS python3.14 m dl util pthread)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux) include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
endif() endif()
# Add the directory where the linker should look for the libraries # Add the directory where the linker should look for the libraries
#link_directories(${CMAKE_SOURCE_DIR}/deps_linux) #link_directories(${CMAKE_SOURCE_DIR}/deps_linux)
link_directories(${CMAKE_SOURCE_DIR}/__lib) link_directories(${CMAKE_SOURCE_DIR}/lib)
# Define the executable target before linking libraries # Define the executable target before linking libraries
add_executable(mcrogueface ${SOURCES}) add_executable(mcrogueface ${SOURCES})
# Define NO_SDL for libtcod-headless headers (excludes SDL-dependent code)
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
# On Windows, set subsystem to WINDOWS to hide console
if(WIN32)
set_target_properties(mcrogueface PROPERTIES
WIN32_EXECUTABLE TRUE
LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup")
endif()
# Now the linker will find the libraries in the specified directory # Now the linker will find the libraries in the specified directory
target_link_libraries(mcrogueface ${LINK_LIBS}) target_link_libraries(mcrogueface ${LINK_LIBS})
@ -96,28 +67,9 @@ add_custom_command(TARGET mcrogueface POST_BUILD
# Copy Python standard library to build directory # Copy Python standard library to build directory
add_custom_command(TARGET mcrogueface POST_BUILD add_custom_command(TARGET mcrogueface POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib) ${CMAKE_SOURCE_DIR}/lib $<TARGET_FILE_DIR:mcrogueface>/lib)
# On Windows, copy DLLs to executable directory # rpath for including shared libraries
if(WIN32) set_target_properties(mcrogueface PROPERTIES
# Copy all DLL files from lib to the executable directory INSTALL_RPATH "./lib")
add_custom_command(TARGET mcrogueface POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E echo "Copied DLLs to executable directory")
# Alternative: Copy specific DLLs if you want more control
# file(GLOB DLLS "${CMAKE_SOURCE_DIR}/__lib/*.dll")
# foreach(DLL ${DLLS})
# add_custom_command(TARGET mcrogueface POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E copy_if_different
# ${DLL} $<TARGET_FILE_DIR:mcrogueface>)
# endforeach()
endif()
# rpath for including shared libraries (Linux/Unix only)
if(NOT WIN32)
set_target_properties(mcrogueface PROPERTIES
INSTALL_RPATH "$ORIGIN/./lib")
endif()

View File

@ -1,54 +0,0 @@
# Convenience Makefile wrapper for McRogueFace
# This delegates to CMake build in the build directory
.PHONY: all build clean run test dist help
# Default target
all: build
# Build the project
build:
@./build.sh
# Clean build artifacts
clean:
@./clean.sh
# Run the game
run: build
@cd build && ./mcrogueface
# Run in Python mode
python: build
@cd build && ./mcrogueface -i
# Test basic functionality
test: build
@echo "Testing McRogueFace..."
@cd build && ./mcrogueface -V
@cd build && ./mcrogueface -c "print('Test passed')"
@cd build && ./mcrogueface --headless -c "import mcrfpy; print('mcrfpy imported successfully')"
# Create distribution archive
dist: build
@echo "Creating distribution archive..."
@cd build && zip -r ../McRogueFace-$$(date +%Y%m%d).zip . -x "*.o" "CMakeFiles/*" "Makefile" "*.cmake"
@echo "Distribution archive created: McRogueFace-$$(date +%Y%m%d).zip"
# Show help
help:
@echo "McRogueFace Build System"
@echo "======================="
@echo ""
@echo "Available targets:"
@echo " make - Build the project (default)"
@echo " make build - Build the project"
@echo " make clean - Remove all build artifacts"
@echo " make run - Build and run the game"
@echo " make python - Build and run in Python interactive mode"
@echo " make test - Run basic tests"
@echo " make dist - Create distribution archive"
@echo " make help - Show this help message"
@echo ""
@echo "Build output goes to: ./build/"
@echo "Distribution archives are created in project root"

198
README.md
View File

@ -1,182 +1,30 @@
# McRogueFace # McRogueFace - 2D Game Engine
An experimental prototype game engine built for my own use in 7DRL 2023.
*Blame my wife for the name* *Blame my wife for the name*
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML. ## Tenets:
* Core roguelike logic from libtcod: field of view, pathfinding * C++ first, Python close behind.
* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera * Entity-Component system based on David Churchill's Memorial University COMP4300 course lectures available on Youtube.
* Simple GUI element system allows keyboard and mouse input, composition * Graphics, particles and shaders provided by SFML.
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship" * Pathfinding, noise generation, and other Roguelike goodness provided by TCOD.
![ Image ]() ## Why?
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items. I did the r/RoguelikeDev TCOD tutorial in Python. I loved it, but I did not want to be limited to ASCII. I want to be able to draw pixels on top of my tiles (like lines or circles) and eventually incorporate even more polish.
## Quick Start ## To-do
**Download**: * ✅ Initial Commit
* ✅ Integrate scene, action, entity, component system from COMP4300 engine
- The entire McRogueFace visual framework: * ✅ Windows / Visual Studio project
- **Sprite**: an image file or one sprite from a shared sprite sheet * ✅ Draw Sprites
- **Caption**: load a font, display text * ✅ Play Sounds
- **Frame**: A rectangle; put other things on it to move or manage GUIs as modules * ✅ Draw UI, spawn entity from Python code
- **Grid**: A 2D array of tiles with zoom + position control * ❌ Python AI for entities (NPCs on set paths, enemies towards player)
- **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path * ✅ Walking / Collision
- **Animation**: Change any property on any of the above over time * ❌ "Boards" (stairs / doors / walk off edge of screen)
* ❌ Cutscenes - interrupt normal controls, text scroll, character portraits
```bash * ❌ Mouse integration - tooltips, zoom, click to select targets, cursors
# Clone and build
git clone <wherever you found this repo>
cd McRogueFace
make
# Run the example game
cd build
./mcrogueface
```
## Building from Source
For most users, pre-built releases are available. If you need to build from source:
### Quick Build (with pre-built dependencies)
Download `build_deps.tar.gz` from the releases page, then:
```bash
git clone <repository-url> McRogueFace
cd McRogueFace
tar -xzf /path/to/build_deps.tar.gz
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
```
### Full Build (compiling all dependencies)
```bash
git clone --recursive <repository-url> McRogueFace
cd McRogueFace
# See BUILD_FROM_SOURCE.md for complete instructions
```
**[BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md)** - Complete build guide including:
- System dependency installation
- Compiling SFML, Python, and libtcod-headless from source
- Creating `build_deps` archives for distribution
- Troubleshooting common build issues
### System Requirements
- **Linux**: Debian/Ubuntu tested; other distros should work
- **Windows**: Supported (see build guide for details)
- **macOS**: Untested
## Example: Creating a Simple Scene
```python
import mcrfpy
# Create a new scene
mcrfpy.createScene("intro")
# Add a text caption
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
caption.size = 48
caption.fill_color = (255, 255, 255)
# Add to scene
mcrfpy.sceneUI("intro").append(caption)
# Switch to the scene
mcrfpy.setScene("intro")
```
## Documentation
### 📚 Developer Documentation
For comprehensive documentation about systems, architecture, and development workflows:
**[Project Wiki](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki)**
Key wiki pages:
- **[Home](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Home)** - Documentation hub with multiple entry points
- **[Grid System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Grid-System)** - Three-layer grid architecture
- **[Python Binding System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Python-Binding-System)** - C++/Python integration
- **[Performance and Profiling](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Performance-and-Profiling)** - Optimization tools
- **[Adding Python Bindings](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Adding-Python-Bindings)** - Step-by-step binding guide
- **[Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap)** - All 46 open issues organized by system
### 📖 Development Guides
In the repository root:
- **[CLAUDE.md](CLAUDE.md)** - Build instructions, testing guidelines, common tasks
- **[ROADMAP.md](ROADMAP.md)** - Strategic vision and development phases
- **[roguelike_tutorial/](roguelike_tutorial/)** - Complete roguelike tutorial implementations
## Build Requirements
- C++17 compiler (GCC 7+ or Clang 5+)
- CMake 3.14+
- Python 3.12+
- SFML 2.6
- Linux or Windows (macOS untested)
## Project Structure
```
McRogueFace/
├── assets/ # Sprites, fonts, audio
├── build/ # Build output directory: zip + ship
│ ├─ (*)assets/ # (copied location of assets)
│ ├─ (*)scripts/ # (copied location of src/scripts)
│ └─ lib/ # SFML, TCOD libraries, Python + standard library / modules
├── deps/ # Python, SFML, and libtcod imports can be tossed in here to build
│ └─ platform/ # windows, linux subdirectories for OS-specific cpython config
├── docs/ # generated HTML, markdown docs
│ └─ stubs/ # .pyi files for editor integration
├── modules/ # git submodules, to build all of McRogueFace's dependencies from source
├── src/ # C++ engine source
│ └─ scripts/ # Python game scripts (copied during build)
└── tests/ # Automated test suite
└── tools/ # For the McRogueFace ecosystem: docs generation
```
If you are building McRogueFace to implement game logic or scene configuration in C++, you'll have to compile the project.
If you are writing a game in Python using McRogueFace, you only need to rename and zip/distribute the `build` directory.
## Philosophy
- **C++ every frame, Python every tick**: All rendering data is handled in C++. Structure your UI and program animations in Python, and they are rendered without Python. All game logic can be written in Python.
- **No Compiling Required; Zip And Ship**: Implement your game objects with Python, zip up McRogueFace with your "game.py" to ship
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
- **Hands-Off Testing**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
## Contributing
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.
### Issue Tracking
The project uses [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for task tracking and bug reports. Issues are organized with labels:
- **System labels** (grid, animation, python-binding, etc.) - identify which codebase area
- **Priority labels** (tier1-active, tier2-foundation, tier3-future) - development timeline
- **Type labels** (Major Feature, Minor Feature, Bugfix, etc.) - effort and scope
See the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap) on the wiki for organized view of all open tasks.
## License
This project is licensed under the MIT License - see LICENSE file for details.
## Acknowledgments
- Developed for 7-Day Roguelike 2023, 2024, 2025 - here's to many more
- Built with [SFML](https://www.sfml-dev.org/), [libtcod](https://github.com/libtcod/libtcod), and Python
- Inspired by David Churchill's COMP4300 game engine lectures

View File

@ -1,223 +0,0 @@
# McRogueFace - Development Roadmap
## Project Status
**Current State**: Active development - C++ game engine with Python scripting
**Latest Release**: Alpha 0.1
**Issue Tracking**: See [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current tasks and bugs
---
## 🎯 Strategic Vision
### Engine Philosophy
- **C++ First**: Performance-critical code stays in C++
- **Python Close Behind**: Rich scripting without frame-rate impact
- **Game-Ready**: Each improvement should benefit actual game development
### Architecture Goals
1. **Clean Inheritance**: Drawable → UI components, proper type preservation
2. **Collection Consistency**: Uniform iteration, indexing, and search patterns
3. **Resource Management**: RAII everywhere, proper lifecycle handling
4. **Multi-Platform**: Windows/Linux feature parity maintained
---
## 🏗️ Architecture Decisions
### Three-Layer Grid Architecture
Following successful roguelike patterns (Caves of Qud, Cogmind, DCSS):
1. **Visual Layer** (UIGridPoint) - Sprites, colors, animations
2. **World State Layer** (TCODMap) - Walkability, transparency, physics
3. **Entity Perspective Layer** (UIGridPointState) - Per-entity FOV, knowledge
### Performance Architecture
Critical for large maps (1000x1000):
- **Spatial Hashing** for entity queries (not quadtrees!)
- **Batch Operations** with context managers (10-100x speedup)
- **Memory Pooling** for entities and components
- **Dirty Flag System** to avoid unnecessary updates
- **Zero-Copy NumPy Integration** via buffer protocol
### Key Insight from Research
"Minimizing Python/C++ boundary crossings matters more than individual function complexity"
- Batch everything possible
- Use context managers for logical operations
- Expose arrays, not individual cells
- Profile and optimize hot paths only
---
## 🚀 Development Phases
For detailed task tracking and current priorities, see the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).
### Phase 1: Foundation Stabilization ✅
**Status**: Complete
**Key Issues**: #7 (Safe Constructors), #71 (Base Class), #87 (Visibility), #88 (Opacity)
### Phase 2: Constructor & API Polish ✅
**Status**: Complete
**Key Features**: Pythonic API, tuple support, standardized defaults
### Phase 3: Entity Lifecycle Management ✅
**Status**: Complete
**Key Issues**: #30 (Entity.die()), #93 (Vector methods), #94 (Color helpers), #103 (Timer objects)
### Phase 4: Visibility & Performance ✅
**Status**: Complete
**Key Features**: AABB culling, name system, profiling tools
### Phase 5: Window/Scene Architecture ✅
**Status**: Complete
**Key Issues**: #34 (Window object), #61 (Scene object), #1 (Resize events), #105 (Scene transitions)
### Phase 6: Rendering Revolution ✅
**Status**: Complete
**Key Issues**: #50 (Grid backgrounds), #6 (RenderTexture), #8 (Viewport rendering)
### Phase 7: Documentation & Distribution ✅
**Status**: Complete (2025-10-30)
**Key Issues**: #85 (Docstrings), #86 (Parameter docs), #108 (Type stubs), #97 (API docs)
**Completed**: All classes and functions converted to MCRF_* macro system with automated HTML/Markdown/man page generation
See [current open issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues?state=open) for active work.
---
## 🔮 Future Vision: Pure Python Extension Architecture
### Concept: McRogueFace as a Traditional Python Package
**Status**: Long-term vision
**Complexity**: Major architectural overhaul
Instead of being a C++ application that embeds Python, McRogueFace could be redesigned as a pure Python extension module that can be installed via `pip install mcrogueface`.
### Technical Approach
1. **Separate Core Engine from Python Embedding**
- Extract SFML rendering, audio, and input into C++ extension modules
- Remove embedded CPython interpreter
- Use Python's C API to expose functionality
2. **Module Structure**
```
mcrfpy/
├── __init__.py # Pure Python coordinator
├── _core.so # C++ rendering/game loop extension
├── _sfml.so # SFML bindings
├── _audio.so # Audio system bindings
└── engine.py # Python game engine logic
```
3. **Inverted Control Flow**
- Python drives the main loop instead of C++
- C++ extensions handle performance-critical operations
- Python manages game logic, scenes, and entity systems
### Benefits
- **Standard Python Packaging**: `pip install mcrogueface`
- **Virtual Environment Support**: Works with venv, conda, poetry
- **Better IDE Integration**: Standard Python development workflow
- **Easier Testing**: Use pytest, standard Python testing tools
- **Cross-Python Compatibility**: Support multiple Python versions
- **Modular Architecture**: Users can import only what they need
### Challenges
- **Major Refactoring**: Complete restructure of codebase
- **Performance Considerations**: Python-driven main loop overhead
- **Build Complexity**: Multiple extension modules to compile
- **Platform Support**: Need wheels for many platform/Python combinations
- **API Stability**: Would need careful design to maintain compatibility
### Example Usage (Future Vision)
```python
import mcrfpy
from mcrfpy import Scene, Frame, Sprite, Grid
# Create game directly in Python
game = mcrfpy.Game(width=1024, height=768)
# Define scenes using Python classes
class MainMenu(Scene):
def on_enter(self):
self.ui.append(Frame(100, 100, 200, 50))
self.ui.append(Sprite("logo.png", x=400, y=100))
def on_keypress(self, key, pressed):
if key == "ENTER" and pressed:
self.game.set_scene("game")
# Run the game
game.add_scene("menu", MainMenu())
game.run()
```
This architecture would make McRogueFace a first-class Python citizen, following standard Python packaging conventions while maintaining high performance through C++ extensions.
---
## 📋 Major Feature Areas
For current status and detailed tasks, see the corresponding Gitea issue labels:
### Core Systems
- **UI/Rendering System**: Issues tagged `[Major Feature]` related to rendering
- **Grid/Entity System**: Pathfinding, FOV, entity management
- **Animation System**: Property animation, easing functions, callbacks
- **Scene/Window Management**: Scene lifecycle, transitions, viewport
### Performance Optimization
- **#115**: SpatialHash for 10,000+ entities
- **#116**: Dirty flag system
- **#113**: Batch operations for NumPy-style access
- **#117**: Memory pool for entities
### Advanced Features
- **#118**: Scene as Drawable (scenes can be drawn/animated)
- **#122**: Parent-Child UI System
- **#123**: Grid Subgrid System (256x256 chunks)
- **#124**: Grid Point Animation
- **#106**: Shader support
- **#107**: Particle system
### Documentation
- **#92**: Inline C++ documentation system
- **#91**: Python type stub files (.pyi)
- **#97**: Automated API documentation extraction
- **#126**: Generate perfectly consistent Python interface
---
## 📚 Resources
- **Issue Tracker**: [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues)
- **Source Code**: [Gitea Repository](https://gamedev.ffwf.net/gitea/john/McRogueFace)
- **Documentation**: See `CLAUDE.md` for build instructions and development guide
- **Tutorial**: See `roguelike_tutorial/` for implementation examples
- **Workflow**: See "Gitea-First Workflow" section in `CLAUDE.md` for issue management best practices
---
## 🔄 Development Workflow
**Gitea is the Single Source of Truth** for this project. Before starting any work:
1. **Check Gitea Issues** for existing tasks, bugs, or related work
2. **Create granular issues** for new features or problems
3. **Update issues** when work affects other systems
4. **Document discoveries** - if something is undocumented or misleading, create a task to fix it
5. **Cross-reference commits** with issue numbers (e.g., "Fixes #104")
See the "Gitea-First Workflow" section in `CLAUDE.md` for detailed guidelines on efficient development practices using the Gitea MCP tools.
---
*For current priorities, task tracking, and bug reports, please use the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,54 +0,0 @@
#!/bin/bash
# Build script for McRogueFace - compiles everything into ./build directory
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}McRogueFace Build Script${NC}"
echo "========================="
# Create build directory if it doesn't exist
if [ ! -d "build" ]; then
echo -e "${YELLOW}Creating build directory...${NC}"
mkdir build
fi
# Change to build directory
cd build
# Run CMake to generate build files
echo -e "${YELLOW}Running CMake...${NC}"
cmake .. -DCMAKE_BUILD_TYPE=Release
# Check if CMake succeeded
if [ $? -ne 0 ]; then
echo -e "${RED}CMake configuration failed!${NC}"
exit 1
fi
# Run make with parallel jobs
echo -e "${YELLOW}Building with make...${NC}"
make -j$(nproc)
# Check if make succeeded
if [ $? -ne 0 ]; then
echo -e "${RED}Build failed!${NC}"
exit 1
fi
echo -e "${GREEN}Build completed successfully!${NC}"
echo ""
echo "The build directory contains:"
ls -la
echo ""
echo -e "${GREEN}To run McRogueFace:${NC}"
echo " cd build"
echo " ./mcrogueface"
echo ""
echo -e "${GREEN}To create a distribution archive:${NC}"
echo " cd build"
echo " zip -r ../McRogueFace-$(date +%Y%m%d).zip ."

View File

@ -1,36 +0,0 @@
@echo off
REM Windows build script for McRogueFace
REM Run this over SSH without Visual Studio GUI
echo Building McRogueFace for Windows...
REM Clean previous build
if exist build_win rmdir /s /q build_win
mkdir build_win
cd build_win
REM Generate Visual Studio project files with CMake
REM Use -G to specify generator, -A for architecture
REM Visual Studio 2022 = "Visual Studio 17 2022"
REM Visual Studio 2019 = "Visual Studio 16 2019"
cmake -G "Visual Studio 17 2022" -A x64 ..
if errorlevel 1 (
echo CMake configuration failed!
exit /b 1
)
REM Build using MSBuild (comes with Visual Studio)
REM You can also use cmake --build . --config Release
msbuild McRogueFace.sln /p:Configuration=Release /p:Platform=x64 /m
if errorlevel 1 (
echo Build failed!
exit /b 1
)
echo Build completed successfully!
echo Executable location: build_win\Release\mcrogueface.exe
REM Alternative: Using cmake to build (works with any generator)
REM cmake --build . --config Release --parallel
cd ..

View File

@ -1,42 +0,0 @@
@echo off
REM Windows build script using cmake --build (generator-agnostic)
REM This version works with any CMake generator
echo Building McRogueFace for Windows using CMake...
REM Set build directory
set BUILD_DIR=build_win
set CONFIG=Release
REM Clean previous build
if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR%
mkdir %BUILD_DIR%
cd %BUILD_DIR%
REM Configure with CMake
REM You can change the generator here if needed:
REM -G "Visual Studio 17 2022" (VS 2022)
REM -G "Visual Studio 16 2019" (VS 2019)
REM -G "MinGW Makefiles" (MinGW)
REM -G "Ninja" (Ninja build system)
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%CONFIG% ..
if errorlevel 1 (
echo CMake configuration failed!
cd ..
exit /b 1
)
REM Build using cmake (works with any generator)
cmake --build . --config %CONFIG% --parallel
if errorlevel 1 (
echo Build failed!
cd ..
exit /b 1
)
echo.
echo Build completed successfully!
echo Executable: %BUILD_DIR%\%CONFIG%\mcrogueface.exe
echo.
cd ..

View File

@ -1,112 +0,0 @@
[
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/GameEngine.cpp.o -c /home/john/Development/McRogueFace/src/GameEngine.cpp",
"file": "/home/john/Development/McRogueFace/src/GameEngine.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/IndexTexture.cpp.o -c /home/john/Development/McRogueFace/src/IndexTexture.cpp",
"file": "/home/john/Development/McRogueFace/src/IndexTexture.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/McRFPy_API.cpp.o -c /home/john/Development/McRogueFace/src/McRFPy_API.cpp",
"file": "/home/john/Development/McRogueFace/src/McRFPy_API.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyCallable.cpp.o -c /home/john/Development/McRogueFace/src/PyCallable.cpp",
"file": "/home/john/Development/McRogueFace/src/PyCallable.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyColor.cpp.o -c /home/john/Development/McRogueFace/src/PyColor.cpp",
"file": "/home/john/Development/McRogueFace/src/PyColor.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyFont.cpp.o -c /home/john/Development/McRogueFace/src/PyFont.cpp",
"file": "/home/john/Development/McRogueFace/src/PyFont.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyScene.cpp.o -c /home/john/Development/McRogueFace/src/PyScene.cpp",
"file": "/home/john/Development/McRogueFace/src/PyScene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyTexture.cpp.o -c /home/john/Development/McRogueFace/src/PyTexture.cpp",
"file": "/home/john/Development/McRogueFace/src/PyTexture.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyVector.cpp.o -c /home/john/Development/McRogueFace/src/PyVector.cpp",
"file": "/home/john/Development/McRogueFace/src/PyVector.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Resources.cpp.o -c /home/john/Development/McRogueFace/src/Resources.cpp",
"file": "/home/john/Development/McRogueFace/src/Resources.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Scene.cpp.o -c /home/john/Development/McRogueFace/src/Scene.cpp",
"file": "/home/john/Development/McRogueFace/src/Scene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Timer.cpp.o -c /home/john/Development/McRogueFace/src/Timer.cpp",
"file": "/home/john/Development/McRogueFace/src/Timer.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICaption.cpp.o -c /home/john/Development/McRogueFace/src/UICaption.cpp",
"file": "/home/john/Development/McRogueFace/src/UICaption.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICollection.cpp.o -c /home/john/Development/McRogueFace/src/UICollection.cpp",
"file": "/home/john/Development/McRogueFace/src/UICollection.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIDrawable.cpp.o -c /home/john/Development/McRogueFace/src/UIDrawable.cpp",
"file": "/home/john/Development/McRogueFace/src/UIDrawable.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIEntity.cpp.o -c /home/john/Development/McRogueFace/src/UIEntity.cpp",
"file": "/home/john/Development/McRogueFace/src/UIEntity.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIFrame.cpp.o -c /home/john/Development/McRogueFace/src/UIFrame.cpp",
"file": "/home/john/Development/McRogueFace/src/UIFrame.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGrid.cpp.o -c /home/john/Development/McRogueFace/src/UIGrid.cpp",
"file": "/home/john/Development/McRogueFace/src/UIGrid.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGridPoint.cpp.o -c /home/john/Development/McRogueFace/src/UIGridPoint.cpp",
"file": "/home/john/Development/McRogueFace/src/UIGridPoint.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UISprite.cpp.o -c /home/john/Development/McRogueFace/src/UISprite.cpp",
"file": "/home/john/Development/McRogueFace/src/UISprite.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UITestScene.cpp.o -c /home/john/Development/McRogueFace/src/UITestScene.cpp",
"file": "/home/john/Development/McRogueFace/src/UITestScene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/main.cpp.o -c /home/john/Development/McRogueFace/src/main.cpp",
"file": "/home/john/Development/McRogueFace/src/main.cpp"
}
]

157
css_colors.txt Normal file
View File

@ -0,0 +1,157 @@
aqua #00FFFF
black #000000
blue #0000FF
fuchsia #FF00FF
gray #808080
green #008000
lime #00FF00
maroon #800000
navy #000080
olive #808000
purple #800080
red #FF0000
silver #C0C0C0
teal #008080
white #FFFFFF
yellow #FFFF00
aliceblue #F0F8FF
antiquewhite #FAEBD7
aqua #00FFFF
aquamarine #7FFFD4
azure #F0FFFF
beige #F5F5DC
bisque #FFE4C4
black #000000
blanchedalmond #FFEBCD
blue #0000FF
blueviolet #8A2BE2
brown #A52A2A
burlywood #DEB887
cadetblue #5F9EA0
chartreuse #7FFF00
chocolate #D2691E
coral #FF7F50
cornflowerblue #6495ED
cornsilk #FFF8DC
crimson #DC143C
cyan #00FFFF
darkblue #00008B
darkcyan #008B8B
darkgoldenrod #B8860B
darkgray #A9A9A9
darkgreen #006400
darkkhaki #BDB76B
darkmagenta #8B008B
darkolivegreen #556B2F
darkorange #FF8C00
darkorchid #9932CC
darkred #8B0000
darksalmon #E9967A
darkseagreen #8FBC8F
darkslateblue #483D8B
darkslategray #2F4F4F
darkturquoise #00CED1
darkviolet #9400D3
deeppink #FF1493
deepskyblue #00BFFF
dimgray #696969
dodgerblue #1E90FF
firebrick #B22222
floralwhite #FFFAF0
forestgreen #228B22
fuchsia #FF00FF
gainsboro #DCDCDC
ghostwhite #F8F8FF
gold #FFD700
goldenrod #DAA520
gray #7F7F7F
green #008000
greenyellow #ADFF2F
honeydew #F0FFF0
hotpink #FF69B4
indianred #CD5C5C
indigo #4B0082
ivory #FFFFF0
khaki #F0E68C
lavender #E6E6FA
lavenderblush #FFF0F5
lawngreen #7CFC00
lemonchiffon #FFFACD
lightblue #ADD8E6
lightcoral #F08080
lightcyan #E0FFFF
lightgoldenrodyellow #FAFAD2
lightgreen #90EE90
lightgrey #D3D3D3
lightpink #FFB6C1
lightsalmon #FFA07A
lightseagreen #20B2AA
lightskyblue #87CEFA
lightslategray #778899
lightsteelblue #B0C4DE
lightyellow #FFFFE0
lime #00FF00
limegreen #32CD32
linen #FAF0E6
magenta #FF00FF
maroon #800000
mediumaquamarine #66CDAA
mediumblue #0000CD
mediumorchid #BA55D3
mediumpurple #9370DB
mediumseagreen #3CB371
mediumslateblue #7B68EE
mediumspringgreen #00FA9A
mediumturquoise #48D1CC
mediumvioletred #C71585
midnightblue #191970
mintcream #F5FFFA
mistyrose #FFE4E1
moccasin #FFE4B5
navajowhite #FFDEAD
navy #000080
navyblue #9FAFDF
oldlace #FDF5E6
olive #808000
olivedrab #6B8E23
orange #FFA500
orangered #FF4500
orchid #DA70D6
palegoldenrod #EEE8AA
palegreen #98FB98
paleturquoise #AFEEEE
palevioletred #DB7093
papayawhip #FFEFD5
peachpuff #FFDAB9
peru #CD853F
pink #FFC0CB
plum #DDA0DD
powderblue #B0E0E6
purple #800080
red #FF0000
rosybrown #BC8F8F
royalblue #4169E1
saddlebrown #8B4513
salmon #FA8072
sandybrown #FA8072
seagreen #2E8B57
seashell #FFF5EE
sienna #A0522D
silver #C0C0C0
skyblue #87CEEB
slateblue #6A5ACD
slategray #708090
snow #FFFAFA
springgreen #00FF7F
steelblue #4682B4
tan #D2B48C
teal #008080
thistle #D8BFD8
tomato #FF6347
turquoise #40E0D0
violet #EE82EE
wheat #F5DEB3
white #FFFFFF
whitesmoke #F5F5F5
yellow #FFFF00
yellowgreen #9ACD32

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,532 +0,0 @@
"""Type stubs for McRogueFace Python API.
Core game engine interface for creating roguelike games with Python.
"""
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
# Type aliases
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid']
Transition = Union[str, None]
# Classes
class Color:
"""SFML Color Object for RGBA colors."""
r: int
g: int
b: int
a: int
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
def from_hex(self, hex_string: str) -> 'Color':
"""Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
...
def to_hex(self) -> str:
"""Convert color to hex string format."""
...
def lerp(self, other: 'Color', t: float) -> 'Color':
"""Linear interpolation between two colors."""
...
class Vector:
"""SFML Vector Object for 2D coordinates."""
x: float
y: float
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float, y: float) -> None: ...
def add(self, other: 'Vector') -> 'Vector': ...
def subtract(self, other: 'Vector') -> 'Vector': ...
def multiply(self, scalar: float) -> 'Vector': ...
def divide(self, scalar: float) -> 'Vector': ...
def distance(self, other: 'Vector') -> float: ...
def normalize(self) -> 'Vector': ...
def dot(self, other: 'Vector') -> float: ...
class Texture:
"""SFML Texture Object for images."""
def __init__(self, filename: str) -> None: ...
filename: str
width: int
height: int
sprite_count: int
class Font:
"""SFML Font Object for text rendering."""
def __init__(self, filename: str) -> None: ...
filename: str
family: str
class Drawable:
"""Base class for all drawable UI elements."""
x: float
y: float
visible: bool
z_index: int
name: str
pos: Vector
def get_bounds(self) -> Tuple[float, float, float, float]:
"""Get bounding box as (x, y, width, height)."""
...
def move(self, dx: float, dy: float) -> None:
"""Move by relative offset (dx, dy)."""
...
def resize(self, width: float, height: float) -> None:
"""Resize to new dimensions (width, height)."""
...
class Frame(Drawable):
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)
A rectangular frame UI element that can contain other drawable elements.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0,
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
outline: float = 0, click: Optional[Callable] = None,
children: Optional[List[UIElement]] = None) -> None: ...
w: float
h: float
fill_color: Color
outline_color: Color
outline: float
click: Optional[Callable[[float, float, int], None]]
children: 'UICollection'
clip_children: bool
class Caption(Drawable):
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)
A text display UI element with customizable font and styling.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, text: str = '', x: float = 0, y: float = 0,
font: Optional[Font] = None, fill_color: Optional[Color] = None,
outline_color: Optional[Color] = None, outline: float = 0,
click: Optional[Callable] = None) -> None: ...
text: str
font: Font
fill_color: Color
outline_color: Color
outline: float
click: Optional[Callable[[float, float, int], None]]
w: float # Read-only, computed from text
h: float # Read-only, computed from text
class Sprite(Drawable):
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)
A sprite UI element that displays a texture or portion of a texture atlas.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None,
sprite_index: int = 0, scale: float = 1.0,
click: Optional[Callable] = None) -> None: ...
texture: Texture
sprite_index: int
scale: float
click: Optional[Callable[[float, float, int], None]]
w: float # Read-only, computed from texture
h: float # Read-only, computed from texture
class Grid(Drawable):
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)
A grid-based tilemap UI element for rendering tile-based levels and game worlds.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20),
texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16,
scale: float = 1.0, click: Optional[Callable] = None) -> None: ...
grid_size: Tuple[int, int]
tile_width: int
tile_height: int
texture: Texture
scale: float
points: List[List['GridPoint']]
entities: 'EntityCollection'
background_color: Color
click: Optional[Callable[[int, int, int], None]]
def at(self, x: int, y: int) -> 'GridPoint':
"""Get grid point at tile coordinates."""
...
class GridPoint:
"""Grid point representing a single tile."""
texture_index: int
solid: bool
color: Color
class GridPointState:
"""State information for a grid point."""
texture_index: int
color: Color
class Entity(Drawable):
"""Entity(grid_x=0, grid_y=0, texture=None, sprite_index=0, name='')
Game entity that lives within a Grid.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, grid_x: float = 0, grid_y: float = 0, texture: Optional[Texture] = None,
sprite_index: int = 0, name: str = '') -> None: ...
grid_x: float
grid_y: float
texture: Texture
sprite_index: int
grid: Optional[Grid]
def at(self, grid_x: float, grid_y: float) -> None:
"""Move entity to grid position."""
...
def die(self) -> None:
"""Remove entity from its grid."""
...
def index(self) -> int:
"""Get index in parent grid's entity collection."""
...
class UICollection:
"""Collection of UI drawable elements (Frame, Caption, Sprite, Grid)."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> UIElement: ...
def __setitem__(self, index: int, value: UIElement) -> None: ...
def __delitem__(self, index: int) -> None: ...
def __contains__(self, item: UIElement) -> bool: ...
def __iter__(self) -> Any: ...
def __add__(self, other: 'UICollection') -> 'UICollection': ...
def __iadd__(self, other: 'UICollection') -> 'UICollection': ...
def append(self, item: UIElement) -> None: ...
def extend(self, items: List[UIElement]) -> None: ...
def remove(self, item: UIElement) -> None: ...
def index(self, item: UIElement) -> int: ...
def count(self, item: UIElement) -> int: ...
class EntityCollection:
"""Collection of Entity objects."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> Entity: ...
def __setitem__(self, index: int, value: Entity) -> None: ...
def __delitem__(self, index: int) -> None: ...
def __contains__(self, item: Entity) -> bool: ...
def __iter__(self) -> Any: ...
def __add__(self, other: 'EntityCollection') -> 'EntityCollection': ...
def __iadd__(self, other: 'EntityCollection') -> 'EntityCollection': ...
def append(self, item: Entity) -> None: ...
def extend(self, items: List[Entity]) -> None: ...
def remove(self, item: Entity) -> None: ...
def index(self, item: Entity) -> int: ...
def count(self, item: Entity) -> int: ...
class Scene:
"""Base class for object-oriented scenes."""
name: str
def __init__(self, name: str) -> None: ...
def activate(self) -> None:
"""Called when scene becomes active."""
...
def deactivate(self) -> None:
"""Called when scene becomes inactive."""
...
def get_ui(self) -> UICollection:
"""Get UI elements collection."""
...
def on_keypress(self, key: str, pressed: bool) -> None:
"""Handle keyboard events."""
...
def on_click(self, x: float, y: float, button: int) -> None:
"""Handle mouse clicks."""
...
def on_enter(self) -> None:
"""Called when entering the scene."""
...
def on_exit(self) -> None:
"""Called when leaving the scene."""
...
def on_resize(self, width: int, height: int) -> None:
"""Handle window resize events."""
...
def update(self, dt: float) -> None:
"""Update scene logic."""
...
class Timer:
"""Timer object for scheduled callbacks."""
name: str
interval: int
active: bool
def __init__(self, name: str, callback: Callable[[float], None], interval: int) -> None: ...
def pause(self) -> None:
"""Pause the timer."""
...
def resume(self) -> None:
"""Resume the timer."""
...
def cancel(self) -> None:
"""Cancel and remove the timer."""
...
class Window:
"""Window singleton for managing the game window."""
resolution: Tuple[int, int]
fullscreen: bool
vsync: bool
title: str
fps_limit: int
game_resolution: Tuple[int, int]
scaling_mode: str
@staticmethod
def get() -> 'Window':
"""Get the window singleton instance."""
...
class Animation:
"""Animation object for animating UI properties."""
target: Any
property: str
duration: float
easing: str
loop: bool
on_complete: Optional[Callable]
def __init__(self, target: Any, property: str, start_value: Any, end_value: Any,
duration: float, easing: str = 'linear', loop: bool = False,
on_complete: Optional[Callable] = None) -> None: ...
def start(self) -> None:
"""Start the animation."""
...
def update(self, dt: float) -> bool:
"""Update animation, returns True if still running."""
...
def get_current_value(self) -> Any:
"""Get the current interpolated value."""
...
# Module functions
def createSoundBuffer(filename: str) -> int:
"""Load a sound effect from a file and return its buffer ID."""
...
def loadMusic(filename: str) -> None:
"""Load and immediately play background music from a file."""
...
def setMusicVolume(volume: int) -> None:
"""Set the global music volume (0-100)."""
...
def setSoundVolume(volume: int) -> None:
"""Set the global sound effects volume (0-100)."""
...
def playSound(buffer_id: int) -> None:
"""Play a sound effect using a previously loaded buffer."""
...
def getMusicVolume() -> int:
"""Get the current music volume level (0-100)."""
...
def getSoundVolume() -> int:
"""Get the current sound effects volume level (0-100)."""
...
def sceneUI(scene: Optional[str] = None) -> UICollection:
"""Get all UI elements for a scene."""
...
def currentScene() -> str:
"""Get the name of the currently active scene."""
...
def setScene(scene: str, transition: Optional[str] = None, duration: float = 0.0) -> None:
"""Switch to a different scene with optional transition effect."""
...
def createScene(name: str) -> None:
"""Create a new empty scene."""
...
def keypressScene(handler: Callable[[str, bool], None]) -> None:
"""Set the keyboard event handler for the current scene."""
...
def setTimer(name: str, handler: Callable[[float], None], interval: int) -> None:
"""Create or update a recurring timer."""
...
def delTimer(name: str) -> None:
"""Stop and remove a timer."""
...
def exit() -> None:
"""Cleanly shut down the game engine and exit the application."""
...
def setScale(multiplier: float) -> None:
"""Scale the game window size (deprecated - use Window.resolution)."""
...
def find(name: str, scene: Optional[str] = None) -> Optional[UIElement]:
"""Find the first UI element with the specified name."""
...
def findAll(pattern: str, scene: Optional[str] = None) -> List[UIElement]:
"""Find all UI elements matching a name pattern (supports * wildcards)."""
...
def getMetrics() -> Dict[str, Union[int, float]]:
"""Get current performance metrics."""
...
# Submodule
class automation:
"""Automation API for testing and scripting."""
@staticmethod
def screenshot(filename: str) -> bool:
"""Save a screenshot to the specified file."""
...
@staticmethod
def position() -> Tuple[int, int]:
"""Get current mouse position as (x, y) tuple."""
...
@staticmethod
def size() -> Tuple[int, int]:
"""Get screen size as (width, height) tuple."""
...
@staticmethod
def onScreen(x: int, y: int) -> bool:
"""Check if coordinates are within screen bounds."""
...
@staticmethod
def moveTo(x: int, y: int, duration: float = 0.0) -> None:
"""Move mouse to absolute position."""
...
@staticmethod
def moveRel(xOffset: int, yOffset: int, duration: float = 0.0) -> None:
"""Move mouse relative to current position."""
...
@staticmethod
def dragTo(x: int, y: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse to position."""
...
@staticmethod
def dragRel(xOffset: int, yOffset: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse relative to current position."""
...
@staticmethod
def click(x: Optional[int] = None, y: Optional[int] = None, clicks: int = 1,
interval: float = 0.0, button: str = 'left') -> None:
"""Click mouse at position."""
...
@staticmethod
def mouseDown(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Press mouse button down."""
...
@staticmethod
def mouseUp(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Release mouse button."""
...
@staticmethod
def keyDown(key: str) -> None:
"""Press key down."""
...
@staticmethod
def keyUp(key: str) -> None:
"""Release key."""
...
@staticmethod
def press(key: str) -> None:
"""Press and release a key."""
...
@staticmethod
def typewrite(text: str, interval: float = 0.0) -> None:
"""Type text with optional interval between characters."""
...

View File

@ -1,209 +0,0 @@
"""Type stubs for McRogueFace Python API.
Auto-generated - do not edit directly.
"""
from typing import Any, List, Dict, Tuple, Optional, Callable, Union
# Module documentation
# McRogueFace Python API\n\nCore game engine interface for creating roguelike games with Python.\n\nThis module provides:\n- Scene management (createScene, setScene, currentScene)\n- UI components (Frame, Caption, Sprite, Grid)\n- Entity system for game objects\n- Audio playback (sound effects and music)\n- Timer system for scheduled events\n- Input handling\n- Performance metrics\n\nExample:\n import mcrfpy\n \n # Create a new scene\n mcrfpy.createScene('game')\n mcrfpy.setScene('game')\n \n # Add UI elements\n frame = mcrfpy.Frame(10, 10, 200, 100)\n caption = mcrfpy.Caption('Hello World', 50, 50)\n mcrfpy.sceneUI().extend([frame, caption])\n
# Classes
class Animation:
"""Animation object for animating UI properties"""
def __init__(selftype(self)) -> None: ...
def get_current_value(self, *args, **kwargs) -> Any: ...
def start(self, *args, **kwargs) -> Any: ...
def update(selfreturns True if still running) -> Any: ...
class Caption:
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Color:
"""SFML Color Object"""
def __init__(selftype(self)) -> None: ...
def from_hex(selfe.g., '#FF0000' or 'FF0000') -> Any: ...
def lerp(self, *args, **kwargs) -> Any: ...
def to_hex(self, *args, **kwargs) -> Any: ...
class Drawable:
"""Base class for all drawable UI elements"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Entity:
"""UIEntity objects"""
def __init__(selftype(self)) -> None: ...
def at(self, *args, **kwargs) -> Any: ...
def die(self, *args, **kwargs) -> Any: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def move(selfdx, dy) -> Any: ...
def path_to(selfx: int, y: int) -> bool: ...
def resize(selfwidth, height) -> Any: ...
def update_visibility(self) -> None: ...
class EntityCollection:
"""Iterable, indexable collection of Entities"""
def __init__(selftype(self)) -> None: ...
def append(self, *args, **kwargs) -> Any: ...
def count(self, *args, **kwargs) -> Any: ...
def extend(self, *args, **kwargs) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def remove(self, *args, **kwargs) -> Any: ...
class Font:
"""SFML Font Object"""
def __init__(selftype(self)) -> None: ...
class Frame:
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Grid:
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)"""
def __init__(selftype(self)) -> None: ...
def at(self, *args, **kwargs) -> Any: ...
def compute_astar_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
def compute_dijkstra(selfroot_x: int, root_y: int, diagonal_cost: float = 1.41) -> None: ...
def compute_fov(selfx: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None: ...
def find_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def get_dijkstra_distance(selfx: int, y: int) -> Optional[float]: ...
def get_dijkstra_path(selfx: int, y: int) -> List[Tuple[int, int]]: ...
def is_in_fov(selfx: int, y: int) -> bool: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class GridPoint:
"""UIGridPoint object"""
def __init__(selftype(self)) -> None: ...
class GridPointState:
"""UIGridPointState object"""
def __init__(selftype(self)) -> None: ...
class Scene:
"""Base class for object-oriented scenes"""
def __init__(selftype(self)) -> None: ...
def activate(self, *args, **kwargs) -> Any: ...
def get_ui(self, *args, **kwargs) -> Any: ...
def register_keyboard(selfalternative to overriding on_keypress) -> Any: ...
class Sprite:
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Texture:
"""SFML Texture Object"""
def __init__(selftype(self)) -> None: ...
class Timer:
"""Timer object for scheduled callbacks"""
def __init__(selftype(self)) -> None: ...
def cancel(self, *args, **kwargs) -> Any: ...
def pause(self, *args, **kwargs) -> Any: ...
def restart(self, *args, **kwargs) -> Any: ...
def resume(self, *args, **kwargs) -> Any: ...
class UICollection:
"""Iterable, indexable collection of UI objects"""
def __init__(selftype(self)) -> None: ...
def append(self, *args, **kwargs) -> Any: ...
def count(self, *args, **kwargs) -> Any: ...
def extend(self, *args, **kwargs) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def remove(self, *args, **kwargs) -> Any: ...
class UICollectionIter:
"""Iterator for a collection of UI objects"""
def __init__(selftype(self)) -> None: ...
class UIEntityCollectionIter:
"""Iterator for a collection of UI objects"""
def __init__(selftype(self)) -> None: ...
class Vector:
"""SFML Vector Object"""
def __init__(selftype(self)) -> None: ...
def angle(self, *args, **kwargs) -> Any: ...
def copy(self, *args, **kwargs) -> Any: ...
def distance_to(self, *args, **kwargs) -> Any: ...
def dot(self, *args, **kwargs) -> Any: ...
def magnitude(self, *args, **kwargs) -> Any: ...
def magnitude_squared(self, *args, **kwargs) -> Any: ...
def normalize(self, *args, **kwargs) -> Any: ...
class Window:
"""Window singleton for accessing and modifying the game window properties"""
def __init__(selftype(self)) -> None: ...
def center(self, *args, **kwargs) -> Any: ...
def get(self, *args, **kwargs) -> Any: ...
def screenshot(self, *args, **kwargs) -> Any: ...
# Functions
def createScene(name: str) -> None: ...
def createSoundBuffer(filename: str) -> int: ...
def currentScene() -> str: ...
def delTimer(name: str) -> None: ...
def exit() -> None: ...
def find(name: str, scene: str = None) -> UIDrawable | None: ...
def findAll(pattern: str, scene: str = None) -> list: ...
def getMetrics() -> dict: ...
def getMusicVolume() -> int: ...
def getSoundVolume() -> int: ...
def keypressScene(handler: callable) -> None: ...
def loadMusic(filename: str) -> None: ...
def playSound(buffer_id: int) -> None: ...
def sceneUI(scene: str = None) -> list: ...
def setMusicVolume(volume: int) -> None: ...
def setScale(multiplier: float) -> None: ...
def setScene(scene: str, transition: str = None, duration: float = 0.0) -> None: ...
def setSoundVolume(volume: int) -> None: ...
def setTimer(name: str, handler: callable, interval: int) -> None: ...
# Constants
FOV_BASIC: int
FOV_DIAMOND: int
FOV_PERMISSIVE_0: int
FOV_PERMISSIVE_1: int
FOV_PERMISSIVE_2: int
FOV_PERMISSIVE_3: int
FOV_PERMISSIVE_4: int
FOV_PERMISSIVE_5: int
FOV_PERMISSIVE_6: int
FOV_PERMISSIVE_7: int
FOV_PERMISSIVE_8: int
FOV_RESTRICTIVE: int
FOV_SHADOW: int
default_font: Any
default_texture: Any

View File

@ -1,24 +0,0 @@
"""Type stubs for McRogueFace automation API."""
from typing import Optional, Tuple
def click(x=None, y=None, clicks=1, interval=0.0, button='left') -> Any: ...
def doubleClick(x=None, y=None) -> Any: ...
def dragRel(xOffset, yOffset, duration=0.0, button='left') -> Any: ...
def dragTo(x, y, duration=0.0, button='left') -> Any: ...
def hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c')) -> Any: ...
def keyDown(key) -> Any: ...
def keyUp(key) -> Any: ...
def middleClick(x=None, y=None) -> Any: ...
def mouseDown(x=None, y=None, button='left') -> Any: ...
def mouseUp(x=None, y=None, button='left') -> Any: ...
def moveRel(xOffset, yOffset, duration=0.0) -> Any: ...
def moveTo(x, y, duration=0.0) -> Any: ...
def onScreen(x, y) -> Any: ...
def position() - Get current mouse position as (x, y) -> Any: ...
def rightClick(x=None, y=None) -> Any: ...
def screenshot(filename) -> Any: ...
def scroll(clicks, x=None, y=None) -> Any: ...
def size() - Get screen size as (width, height) -> Any: ...
def tripleClick(x=None, y=None) -> Any: ...
def typewrite(message, interval=0.0) -> Any: ...

View File

Binary file not shown.

@ -1 +1 @@
Subproject commit ebf955df7a89ed0c7968f79faec1de49f61ed7cb Subproject commit 6abddd9f6afdddc09031989e0deb25e301ecf315

@ -1 +1 @@
Subproject commit c6e0284ac58b3f205c95365478888f7b53b077e2 Subproject commit 313676d200f093e2694b5cfca574f72a2b116c85

@ -1 +1 @@
Subproject commit bf9023d1bc6ec422769559a5eff60bd00597354f Subproject commit de565ac8f2b795dedc0307b60830cb006afd2ecd

1
modules/libtcod Submodule

@ -0,0 +1 @@
Subproject commit 34ae258a863c4f6446effee28ca8ecae51b1519f

@ -1 +0,0 @@
Subproject commit 3b4b65dc9aae7d21a98d3578e3e9433728b118bb

View File

@ -11,10 +11,10 @@ public:
const static int WHEEL_NUM = 4; const static int WHEEL_NUM = 4;
const static int WHEEL_NEG = 2; const static int WHEEL_NEG = 2;
const static int WHEEL_DEL = 1; const static int WHEEL_DEL = 1;
static int keycode(const sf::Keyboard::Key& k) { return KEY + (int)k; } static int keycode(sf::Keyboard::Key& k) { return KEY + (int)k; }
static int keycode(const sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; } static int keycode(sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
//static int keycode(sf::Mouse::Wheel& w, float d) { return MOUSEWHEEL + (((int)w)<<12) + int(d*16) + 512; } //static int keycode(sf::Mouse::Wheel& w, float d) { return MOUSEWHEEL + (((int)w)<<12) + int(d*16) + 512; }
static int keycode(const sf::Mouse::Wheel& w, float d) { static int keycode(sf::Mouse::Wheel& w, float d) {
int neg = 0; int neg = 0;
if (d < 0) { neg = 1; } if (d < 0) { neg = 1; }
return MOUSEWHEEL + (w * WHEEL_NUM) + (neg * WHEEL_NEG) + 1; return MOUSEWHEEL + (w * WHEEL_NUM) + (neg * WHEEL_NEG) + 1;
@ -32,7 +32,7 @@ public:
return (a & WHEEL_DEL) * factor; return (a & WHEEL_DEL) * factor;
} }
static std::string key_str(const sf::Keyboard::Key& keycode) static std::string key_str(sf::Keyboard::Key& keycode)
{ {
switch(keycode) switch(keycode)
{ {

View File

@ -1,681 +0,0 @@
#include "Animation.h"
#include "UIDrawable.h"
#include "UIEntity.h"
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "GameEngine.h"
#include "PythonObjectCache.h"
#include <cmath>
#include <algorithm>
#include <unordered_map>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// Forward declaration of PyAnimation type
namespace mcrfpydef {
extern PyTypeObject PyAnimationType;
}
// Animation implementation
Animation::Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc,
bool delta,
PyObject* callback)
: targetProperty(targetProperty)
, targetValue(targetValue)
, duration(duration)
, easingFunc(easingFunc)
, delta(delta)
, pythonCallback(callback)
{
// Increase reference count for Python callback
if (pythonCallback) {
Py_INCREF(pythonCallback);
}
}
Animation::~Animation() {
// Decrease reference count for Python callback if we still own it
PyObject* callback = pythonCallback;
if (callback) {
pythonCallback = nullptr;
PyGILState_STATE gstate = PyGILState_Ensure();
Py_DECREF(callback);
PyGILState_Release(gstate);
}
// Clean up cache entry
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
}
void Animation::start(std::shared_ptr<UIDrawable> target) {
if (!target) return;
targetWeak = target;
elapsed = 0.0f;
callbackTriggered = false; // Reset callback state
// Capture start value from target
std::visit([this, &target](const auto& targetVal) {
using T = std::decay_t<decltype(targetVal)>;
if constexpr (std::is_same_v<T, float>) {
float value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, int>) {
int value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// For sprite animation, get current sprite index
int value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Color>) {
sf::Color value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
sf::Vector2f value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, std::string>) {
std::string value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
}, targetValue);
}
void Animation::startEntity(std::shared_ptr<UIEntity> target) {
if (!target) return;
entityTargetWeak = target;
elapsed = 0.0f;
callbackTriggered = false; // Reset callback state
// Capture the starting value from the entity
std::visit([this, target](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
float value = 0.0f;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, int>) {
// For entities, we might need to handle sprite_index differently
if (targetProperty == "sprite_index" || targetProperty == "sprite_number") {
startValue = target->sprite.getSpriteIndex();
}
}
// Entities don't support other types yet
}, targetValue);
}
bool Animation::hasValidTarget() const {
return !targetWeak.expired() || !entityTargetWeak.expired();
}
void Animation::clearCallback() {
// Safely clear the callback when PyAnimation is being destroyed
PyObject* callback = pythonCallback;
if (callback) {
pythonCallback = nullptr;
callbackTriggered = true; // Prevent future triggering
PyGILState_STATE gstate = PyGILState_Ensure();
Py_DECREF(callback);
PyGILState_Release(gstate);
}
}
void Animation::complete() {
// Jump to end of animation
elapsed = duration;
// Apply final value
if (auto target = targetWeak.lock()) {
AnimationValue finalValue = interpolate(1.0f);
applyValue(target.get(), finalValue);
}
else if (auto entity = entityTargetWeak.lock()) {
AnimationValue finalValue = interpolate(1.0f);
applyValue(entity.get(), finalValue);
}
}
bool Animation::update(float deltaTime) {
// Try to lock weak_ptr to get shared_ptr
std::shared_ptr<UIDrawable> target = targetWeak.lock();
std::shared_ptr<UIEntity> entity = entityTargetWeak.lock();
// If both are null, target was destroyed
if (!target && !entity) {
return false; // Remove this animation
}
if (isComplete()) {
return false;
}
elapsed += deltaTime;
elapsed = std::min(elapsed, duration);
// Calculate easing value (0.0 to 1.0)
float t = duration > 0 ? elapsed / duration : 1.0f;
float easedT = easingFunc(t);
// Get interpolated value
AnimationValue currentValue = interpolate(easedT);
// Apply to whichever target is valid
if (target) {
applyValue(target.get(), currentValue);
} else if (entity) {
applyValue(entity.get(), currentValue);
}
// Trigger callback when animation completes
// Check pythonCallback again in case it was cleared during update
if (isComplete() && !callbackTriggered && pythonCallback) {
triggerCallback();
}
return !isComplete();
}
AnimationValue Animation::getCurrentValue() const {
float t = duration > 0 ? elapsed / duration : 1.0f;
float easedT = easingFunc(t);
return interpolate(easedT);
}
AnimationValue Animation::interpolate(float t) const {
// Visit the variant to perform type-specific interpolation
return std::visit([this, t](const auto& target) -> AnimationValue {
using T = std::decay_t<decltype(target)>;
if constexpr (std::is_same_v<T, float>) {
// Interpolate float
const float* start = std::get_if<float>(&startValue);
if (!start) return target; // Type mismatch
if (delta) {
return *start + target * t;
} else {
return *start + (target - *start) * t;
}
}
else if constexpr (std::is_same_v<T, int>) {
// Interpolate integer
const int* start = std::get_if<int>(&startValue);
if (!start) return target;
float result;
if (delta) {
result = *start + target * t;
} else {
result = *start + (target - *start) * t;
}
return static_cast<int>(std::round(result));
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// For sprite animation, interpolate through the list
if (target.empty()) return target;
// Map t to an index in the vector
size_t index = static_cast<size_t>(t * (target.size() - 1));
index = std::min(index, target.size() - 1);
return static_cast<int>(target[index]);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
// Interpolate color
const sf::Color* start = std::get_if<sf::Color>(&startValue);
if (!start) return target;
sf::Color result;
if (delta) {
result.r = std::clamp(start->r + target.r * t, 0.0f, 255.0f);
result.g = std::clamp(start->g + target.g * t, 0.0f, 255.0f);
result.b = std::clamp(start->b + target.b * t, 0.0f, 255.0f);
result.a = std::clamp(start->a + target.a * t, 0.0f, 255.0f);
} else {
result.r = start->r + (target.r - start->r) * t;
result.g = start->g + (target.g - start->g) * t;
result.b = start->b + (target.b - start->b) * t;
result.a = start->a + (target.a - start->a) * t;
}
return result;
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
// Interpolate vector
const sf::Vector2f* start = std::get_if<sf::Vector2f>(&startValue);
if (!start) return target;
if (delta) {
return sf::Vector2f(start->x + target.x * t,
start->y + target.y * t);
} else {
return sf::Vector2f(start->x + (target.x - start->x) * t,
start->y + (target.y - start->y) * t);
}
}
else if constexpr (std::is_same_v<T, std::string>) {
// For text, show characters based on t
const std::string* start = std::get_if<std::string>(&startValue);
if (!start) return target;
// If delta mode, append characters from target
if (delta) {
size_t chars = static_cast<size_t>(target.length() * t);
return *start + target.substr(0, chars);
} else {
// Transition from start text to target text
if (t < 0.5f) {
// First half: remove characters from start
size_t chars = static_cast<size_t>(start->length() * (1.0f - t * 2.0f));
return start->substr(0, chars);
} else {
// Second half: add characters to target
size_t chars = static_cast<size_t>(target.length() * ((t - 0.5f) * 2.0f));
return target.substr(0, chars);
}
}
}
return target; // Fallback
}, targetValue);
}
void Animation::applyValue(UIDrawable* target, const AnimationValue& value) {
if (!target) return;
std::visit([this, target](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, int>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, std::string>) {
target->setProperty(targetProperty, val);
}
}, value);
}
void Animation::applyValue(UIEntity* entity, const AnimationValue& value) {
if (!entity) return;
std::visit([this, entity](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
entity->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, int>) {
entity->setProperty(targetProperty, val);
}
// Entities don't support other types yet
}, value);
}
void Animation::triggerCallback() {
if (!pythonCallback) return;
// Ensure we only trigger once
if (callbackTriggered) return;
callbackTriggered = true;
PyGILState_STATE gstate = PyGILState_Ensure();
// TODO: In future, create PyAnimation wrapper for this animation
// For now, pass None for both parameters
PyObject* args = PyTuple_New(2);
Py_INCREF(Py_None);
Py_INCREF(Py_None);
PyTuple_SetItem(args, 0, Py_None); // animation parameter
PyTuple_SetItem(args, 1, Py_None); // target parameter
PyObject* result = PyObject_CallObject(pythonCallback, args);
Py_DECREF(args);
if (!result) {
std::cerr << "Animation callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
// Check if we should exit on exception
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
McRFPy_API::signalPythonException();
}
} else {
Py_DECREF(result);
}
PyGILState_Release(gstate);
}
// Easing functions implementation
namespace EasingFunctions {
float linear(float t) {
return t;
}
float easeIn(float t) {
return t * t;
}
float easeOut(float t) {
return t * (2.0f - t);
}
float easeInOut(float t) {
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
}
// Quadratic
float easeInQuad(float t) {
return t * t;
}
float easeOutQuad(float t) {
return t * (2.0f - t);
}
float easeInOutQuad(float t) {
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
}
// Cubic
float easeInCubic(float t) {
return t * t * t;
}
float easeOutCubic(float t) {
float t1 = t - 1.0f;
return t1 * t1 * t1 + 1.0f;
}
float easeInOutCubic(float t) {
return t < 0.5f ? 4.0f * t * t * t : (t - 1.0f) * (2.0f * t - 2.0f) * (2.0f * t - 2.0f) + 1.0f;
}
// Quartic
float easeInQuart(float t) {
return t * t * t * t;
}
float easeOutQuart(float t) {
float t1 = t - 1.0f;
return 1.0f - t1 * t1 * t1 * t1;
}
float easeInOutQuart(float t) {
return t < 0.5f ? 8.0f * t * t * t * t : 1.0f - 8.0f * (t - 1.0f) * (t - 1.0f) * (t - 1.0f) * (t - 1.0f);
}
// Sine
float easeInSine(float t) {
return 1.0f - std::cos(t * M_PI / 2.0f);
}
float easeOutSine(float t) {
return std::sin(t * M_PI / 2.0f);
}
float easeInOutSine(float t) {
return 0.5f * (1.0f - std::cos(M_PI * t));
}
// Exponential
float easeInExpo(float t) {
return t == 0.0f ? 0.0f : std::pow(2.0f, 10.0f * (t - 1.0f));
}
float easeOutExpo(float t) {
return t == 1.0f ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t);
}
float easeInOutExpo(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
if (t < 0.5f) {
return 0.5f * std::pow(2.0f, 20.0f * t - 10.0f);
} else {
return 1.0f - 0.5f * std::pow(2.0f, -20.0f * t + 10.0f);
}
}
// Circular
float easeInCirc(float t) {
return 1.0f - std::sqrt(1.0f - t * t);
}
float easeOutCirc(float t) {
float t1 = t - 1.0f;
return std::sqrt(1.0f - t1 * t1);
}
float easeInOutCirc(float t) {
if (t < 0.5f) {
return 0.5f * (1.0f - std::sqrt(1.0f - 4.0f * t * t));
} else {
return 0.5f * (std::sqrt(1.0f - (2.0f * t - 2.0f) * (2.0f * t - 2.0f)) + 1.0f);
}
}
// Elastic
float easeInElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.3f;
float a = 1.0f;
float s = p / 4.0f;
float t1 = t - 1.0f;
return -(a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
}
float easeOutElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.3f;
float a = 1.0f;
float s = p / 4.0f;
return a * std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * M_PI) / p) + 1.0f;
}
float easeInOutElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.45f;
float a = 1.0f;
float s = p / 4.0f;
if (t < 0.5f) {
float t1 = 2.0f * t - 1.0f;
return -0.5f * (a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
} else {
float t1 = 2.0f * t - 1.0f;
return a * std::pow(2.0f, -10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p) * 0.5f + 1.0f;
}
}
// Back (overshooting)
float easeInBack(float t) {
const float s = 1.70158f;
return t * t * ((s + 1.0f) * t - s);
}
float easeOutBack(float t) {
const float s = 1.70158f;
float t1 = t - 1.0f;
return t1 * t1 * ((s + 1.0f) * t1 + s) + 1.0f;
}
float easeInOutBack(float t) {
const float s = 1.70158f * 1.525f;
if (t < 0.5f) {
return 0.5f * (4.0f * t * t * ((s + 1.0f) * 2.0f * t - s));
} else {
float t1 = 2.0f * t - 2.0f;
return 0.5f * (t1 * t1 * ((s + 1.0f) * t1 + s) + 2.0f);
}
}
// Bounce
float easeOutBounce(float t) {
if (t < 1.0f / 2.75f) {
return 7.5625f * t * t;
} else if (t < 2.0f / 2.75f) {
float t1 = t - 1.5f / 2.75f;
return 7.5625f * t1 * t1 + 0.75f;
} else if (t < 2.5f / 2.75f) {
float t1 = t - 2.25f / 2.75f;
return 7.5625f * t1 * t1 + 0.9375f;
} else {
float t1 = t - 2.625f / 2.75f;
return 7.5625f * t1 * t1 + 0.984375f;
}
}
float easeInBounce(float t) {
return 1.0f - easeOutBounce(1.0f - t);
}
float easeInOutBounce(float t) {
if (t < 0.5f) {
return 0.5f * easeInBounce(2.0f * t);
} else {
return 0.5f * easeOutBounce(2.0f * t - 1.0f) + 0.5f;
}
}
// Get easing function by name
EasingFunction getByName(const std::string& name) {
static std::unordered_map<std::string, EasingFunction> easingMap = {
{"linear", linear},
{"easeIn", easeIn},
{"easeOut", easeOut},
{"easeInOut", easeInOut},
{"easeInQuad", easeInQuad},
{"easeOutQuad", easeOutQuad},
{"easeInOutQuad", easeInOutQuad},
{"easeInCubic", easeInCubic},
{"easeOutCubic", easeOutCubic},
{"easeInOutCubic", easeInOutCubic},
{"easeInQuart", easeInQuart},
{"easeOutQuart", easeOutQuart},
{"easeInOutQuart", easeInOutQuart},
{"easeInSine", easeInSine},
{"easeOutSine", easeOutSine},
{"easeInOutSine", easeInOutSine},
{"easeInExpo", easeInExpo},
{"easeOutExpo", easeOutExpo},
{"easeInOutExpo", easeInOutExpo},
{"easeInCirc", easeInCirc},
{"easeOutCirc", easeOutCirc},
{"easeInOutCirc", easeInOutCirc},
{"easeInElastic", easeInElastic},
{"easeOutElastic", easeOutElastic},
{"easeInOutElastic", easeInOutElastic},
{"easeInBack", easeInBack},
{"easeOutBack", easeOutBack},
{"easeInOutBack", easeInOutBack},
{"easeInBounce", easeInBounce},
{"easeOutBounce", easeOutBounce},
{"easeInOutBounce", easeInOutBounce}
};
auto it = easingMap.find(name);
if (it != easingMap.end()) {
return it->second;
}
return linear; // Default to linear
}
} // namespace EasingFunctions
// AnimationManager implementation
AnimationManager& AnimationManager::getInstance() {
static AnimationManager instance;
return instance;
}
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
if (animation && animation->hasValidTarget()) {
if (isUpdating) {
// Defer adding during update to avoid iterator invalidation
pendingAnimations.push_back(animation);
} else {
activeAnimations.push_back(animation);
}
}
}
void AnimationManager::update(float deltaTime) {
// Set flag to defer new animations
isUpdating = true;
// Remove completed or invalid animations
activeAnimations.erase(
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
[deltaTime](std::shared_ptr<Animation>& anim) {
return !anim || !anim->update(deltaTime);
}),
activeAnimations.end()
);
// Clear update flag
isUpdating = false;
// Add any animations that were created during update
if (!pendingAnimations.empty()) {
activeAnimations.insert(activeAnimations.end(),
pendingAnimations.begin(),
pendingAnimations.end());
pendingAnimations.clear();
}
}
void AnimationManager::clear(bool completeAnimations) {
if (completeAnimations) {
// Complete all animations before clearing
for (auto& anim : activeAnimations) {
if (anim) {
anim->complete();
}
}
}
activeAnimations.clear();
}

View File

@ -1,175 +0,0 @@
#pragma once
#include <string>
#include <functional>
#include <memory>
#include <variant>
#include <vector>
#include <SFML/Graphics.hpp>
#include "Python.h"
// Forward declarations
class UIDrawable;
class UIEntity;
// Forward declare namespace
namespace EasingFunctions {
float linear(float t);
}
// Easing function type
typedef std::function<float(float)> EasingFunction;
// Animation target value can be various types
typedef std::variant<
float, // Single float value
int, // Single integer value
std::vector<int>, // List of integers (for sprite animation)
sf::Color, // Color animation
sf::Vector2f, // Vector animation
std::string // String animation (for text)
> AnimationValue;
class Animation {
public:
// Constructor
Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc = EasingFunctions::linear,
bool delta = false,
PyObject* callback = nullptr);
// Destructor - cleanup Python callback reference
~Animation();
// Apply this animation to a drawable
void start(std::shared_ptr<UIDrawable> target);
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
void startEntity(std::shared_ptr<UIEntity> target);
// Complete the animation immediately (jump to final value)
void complete();
// Update animation (called each frame)
// Returns true if animation is still running, false if complete
bool update(float deltaTime);
// Get current interpolated value
AnimationValue getCurrentValue() const;
// Check if animation has valid target
bool hasValidTarget() const;
// Clear the callback (called when PyAnimation is deallocated)
void clearCallback();
// Animation properties
std::string getTargetProperty() const { return targetProperty; }
float getDuration() const { return duration; }
float getElapsed() const { return elapsed; }
bool isComplete() const { return elapsed >= duration; }
bool isDelta() const { return delta; }
private:
std::string targetProperty; // Property name to animate (e.g., "x", "color.r", "sprite_number")
AnimationValue startValue; // Starting value (captured when animation starts)
AnimationValue targetValue; // Target value to animate to
float duration; // Animation duration in seconds
float elapsed = 0.0f; // Elapsed time
EasingFunction easingFunc; // Easing function to use
bool delta; // If true, targetValue is relative to start
// RAII: Use weak_ptr for safe target tracking
std::weak_ptr<UIDrawable> targetWeak;
std::weak_ptr<UIEntity> entityTargetWeak;
// Callback support
PyObject* pythonCallback = nullptr; // Python callback function (we own a reference)
bool callbackTriggered = false; // Ensure callback only fires once
PyObject* pyAnimationWrapper = nullptr; // Weak reference to PyAnimation if created from Python
// Python object cache support
uint64_t serial_number = 0;
// Helper to interpolate between values
AnimationValue interpolate(float t) const;
// Helper to apply value to target
void applyValue(UIDrawable* target, const AnimationValue& value);
void applyValue(UIEntity* entity, const AnimationValue& value);
// Trigger callback when animation completes
void triggerCallback();
};
// Easing functions library
namespace EasingFunctions {
// Basic easing functions
float linear(float t);
float easeIn(float t);
float easeOut(float t);
float easeInOut(float t);
// Advanced easing functions
float easeInQuad(float t);
float easeOutQuad(float t);
float easeInOutQuad(float t);
float easeInCubic(float t);
float easeOutCubic(float t);
float easeInOutCubic(float t);
float easeInQuart(float t);
float easeOutQuart(float t);
float easeInOutQuart(float t);
float easeInSine(float t);
float easeOutSine(float t);
float easeInOutSine(float t);
float easeInExpo(float t);
float easeOutExpo(float t);
float easeInOutExpo(float t);
float easeInCirc(float t);
float easeOutCirc(float t);
float easeInOutCirc(float t);
float easeInElastic(float t);
float easeOutElastic(float t);
float easeInOutElastic(float t);
float easeInBack(float t);
float easeOutBack(float t);
float easeInOutBack(float t);
float easeInBounce(float t);
float easeOutBounce(float t);
float easeInOutBounce(float t);
// Get easing function by name
EasingFunction getByName(const std::string& name);
}
// Animation manager to handle active animations
class AnimationManager {
public:
static AnimationManager& getInstance();
// Add an animation to be managed
void addAnimation(std::shared_ptr<Animation> animation);
// Update all animations
void update(float deltaTime);
// Clear all animations (optionally completing them first)
void clear(bool completeAnimations = false);
private:
AnimationManager() = default;
std::vector<std::shared_ptr<Animation>> activeAnimations;
std::vector<std::shared_ptr<Animation>> pendingAnimations; // Animations to add after update
bool isUpdating = false; // Flag to track if we're in update loop
};

View File

@ -1,38 +0,0 @@
#include "BenchmarkLogger.h"
#include "GameEngine.h"
// Global benchmark logger instance
BenchmarkLogger g_benchmarkLogger;
void BenchmarkLogger::recordFrame(const ProfilingMetrics& metrics) {
if (!running) return;
auto now = std::chrono::high_resolution_clock::now();
double timestamp_ms = std::chrono::duration<double, std::milli>(now - start_time).count();
BenchmarkFrame frame;
frame.frame_number = ++frame_counter;
frame.timestamp_ms = timestamp_ms;
frame.frame_time_ms = metrics.frameTime;
frame.fps = metrics.fps;
frame.work_time_ms = metrics.workTime;
frame.grid_render_ms = metrics.gridRenderTime;
frame.entity_render_ms = metrics.entityRenderTime;
frame.python_time_ms = metrics.pythonScriptTime;
frame.animation_time_ms = metrics.animationTime;
frame.fov_overlay_ms = metrics.fovOverlayTime;
frame.draw_calls = metrics.drawCalls;
frame.ui_elements = metrics.uiElements;
frame.visible_elements = metrics.visibleElements;
frame.grid_cells_rendered = metrics.gridCellsRendered;
frame.entities_rendered = metrics.entitiesRendered;
frame.total_entities = metrics.totalEntities;
// Move pending logs to this frame
frame.logs = std::move(pending_logs);
pending_logs.clear();
frames.push_back(std::move(frame));
}

View File

@ -1,245 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include <chrono>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <stdexcept>
#ifdef _WIN32
#include <process.h>
#define getpid _getpid
#else
#include <unistd.h>
#endif
// Forward declaration
struct ProfilingMetrics;
/**
* @brief Frame data captured during benchmarking
*/
struct BenchmarkFrame {
int frame_number;
double timestamp_ms; // Time since benchmark start
float frame_time_ms;
int fps;
// Detailed timing breakdown
float work_time_ms; // Actual work time (frame_time - sleep_time)
float grid_render_ms;
float entity_render_ms;
float python_time_ms;
float animation_time_ms;
float fov_overlay_ms;
// Counts
int draw_calls;
int ui_elements;
int visible_elements;
int grid_cells_rendered;
int entities_rendered;
int total_entities;
// User-provided log messages for this frame
std::vector<std::string> logs;
};
/**
* @brief Benchmark logging system for capturing performance data to JSON files
*
* Usage from Python:
* mcrfpy.start_benchmark() # Start capturing
* mcrfpy.log_benchmark("msg") # Add comment to current frame
* filename = mcrfpy.end_benchmark() # Stop and get filename
*/
class BenchmarkLogger {
private:
bool running;
std::string filename;
std::chrono::high_resolution_clock::time_point start_time;
std::vector<BenchmarkFrame> frames;
std::vector<std::string> pending_logs; // Logs for current frame (before it's recorded)
int frame_counter;
// Generate filename based on PID and timestamp
std::string generateFilename() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::tm tm = *std::localtime(&time_t);
std::ostringstream oss;
oss << "benchmark_" << getpid() << "_"
<< std::put_time(&tm, "%Y%m%d_%H%M%S") << ".json";
return oss.str();
}
// Get current timestamp as ISO 8601 string
std::string getCurrentTimestamp() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::tm tm = *std::localtime(&time_t);
std::ostringstream oss;
oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S");
return oss.str();
}
// Escape string for JSON
std::string escapeJson(const std::string& str) {
std::ostringstream oss;
for (char c : str) {
switch (c) {
case '"': oss << "\\\""; break;
case '\\': oss << "\\\\"; break;
case '\b': oss << "\\b"; break;
case '\f': oss << "\\f"; break;
case '\n': oss << "\\n"; break;
case '\r': oss << "\\r"; break;
case '\t': oss << "\\t"; break;
default:
if ('\x00' <= c && c <= '\x1f') {
oss << "\\u" << std::hex << std::setw(4) << std::setfill('0') << (int)c;
} else {
oss << c;
}
}
}
return oss.str();
}
std::string start_timestamp;
public:
BenchmarkLogger() : running(false), frame_counter(0) {}
/**
* @brief Start benchmark logging
* @throws std::runtime_error if already running
*/
void start() {
if (running) {
throw std::runtime_error("Benchmark already running. Call end_benchmark() first.");
}
running = true;
filename = generateFilename();
start_time = std::chrono::high_resolution_clock::now();
start_timestamp = getCurrentTimestamp();
frames.clear();
pending_logs.clear();
frame_counter = 0;
}
/**
* @brief Stop benchmark logging and write to file
* @return The filename that was written
* @throws std::runtime_error if not running
*/
std::string end() {
if (!running) {
throw std::runtime_error("No benchmark running. Call start_benchmark() first.");
}
running = false;
// Calculate duration
auto end_time = std::chrono::high_resolution_clock::now();
double duration_seconds = std::chrono::duration<double>(end_time - start_time).count();
std::string end_timestamp = getCurrentTimestamp();
// Write JSON file
std::ofstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("Failed to open benchmark file for writing: " + filename);
}
file << "{\n";
file << " \"benchmark\": {\n";
file << " \"pid\": " << getpid() << ",\n";
file << " \"start_time\": \"" << start_timestamp << "\",\n";
file << " \"end_time\": \"" << end_timestamp << "\",\n";
file << " \"total_frames\": " << frames.size() << ",\n";
file << " \"duration_seconds\": " << std::fixed << std::setprecision(3) << duration_seconds << "\n";
file << " },\n";
file << " \"frames\": [\n";
for (size_t i = 0; i < frames.size(); ++i) {
const auto& f = frames[i];
file << " {\n";
file << " \"frame_number\": " << f.frame_number << ",\n";
file << " \"timestamp_ms\": " << std::fixed << std::setprecision(3) << f.timestamp_ms << ",\n";
file << " \"frame_time_ms\": " << std::setprecision(3) << f.frame_time_ms << ",\n";
file << " \"fps\": " << f.fps << ",\n";
file << " \"work_time_ms\": " << std::setprecision(3) << f.work_time_ms << ",\n";
file << " \"grid_render_ms\": " << std::setprecision(3) << f.grid_render_ms << ",\n";
file << " \"entity_render_ms\": " << std::setprecision(3) << f.entity_render_ms << ",\n";
file << " \"python_time_ms\": " << std::setprecision(3) << f.python_time_ms << ",\n";
file << " \"animation_time_ms\": " << std::setprecision(3) << f.animation_time_ms << ",\n";
file << " \"fov_overlay_ms\": " << std::setprecision(3) << f.fov_overlay_ms << ",\n";
file << " \"draw_calls\": " << f.draw_calls << ",\n";
file << " \"ui_elements\": " << f.ui_elements << ",\n";
file << " \"visible_elements\": " << f.visible_elements << ",\n";
file << " \"grid_cells_rendered\": " << f.grid_cells_rendered << ",\n";
file << " \"entities_rendered\": " << f.entities_rendered << ",\n";
file << " \"total_entities\": " << f.total_entities << ",\n";
// Write logs array
file << " \"logs\": [";
for (size_t j = 0; j < f.logs.size(); ++j) {
file << "\"" << escapeJson(f.logs[j]) << "\"";
if (j < f.logs.size() - 1) file << ", ";
}
file << "]\n";
file << " }";
if (i < frames.size() - 1) file << ",";
file << "\n";
}
file << " ]\n";
file << "}\n";
file.close();
std::string result = filename;
filename.clear();
frames.clear();
pending_logs.clear();
frame_counter = 0;
return result;
}
/**
* @brief Add a log message to the current frame
* @param message The message to log
* @throws std::runtime_error if not running
*/
void log(const std::string& message) {
if (!running) {
throw std::runtime_error("No benchmark running. Call start_benchmark() first.");
}
pending_logs.push_back(message);
}
/**
* @brief Record frame data (called by game loop at end of each frame)
* @param metrics The current frame's profiling metrics
*/
void recordFrame(const ProfilingMetrics& metrics);
/**
* @brief Check if benchmark is currently running
*/
bool isRunning() const { return running; }
/**
* @brief Get current frame count
*/
int getFrameCount() const { return frame_counter; }
};
// Global benchmark logger instance
extern BenchmarkLogger g_benchmarkLogger;

View File

@ -1,180 +0,0 @@
#include "CommandLineParser.h"
#include <iostream>
#include <filesystem>
#include <algorithm>
CommandLineParser::CommandLineParser(int argc, char* argv[])
: argc(argc), argv(argv) {}
CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) {
ParseResult result;
current_arg = 1; // Reset for each parse
// Detect if running as Python interpreter
std::filesystem::path exec_name = std::filesystem::path(argv[0]).filename();
if (exec_name.string().find("python") == 0) {
config.headless = true;
config.python_mode = true;
}
while (current_arg < argc) {
std::string arg = argv[current_arg];
// Handle Python-style single-letter flags
if (arg == "-h" || arg == "--help") {
print_help();
result.should_exit = true;
result.exit_code = 0;
return result;
}
if (arg == "-V" || arg == "--version") {
print_version();
result.should_exit = true;
result.exit_code = 0;
return result;
}
// Python execution modes
if (arg == "-c") {
config.python_mode = true;
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the -c option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.python_command = argv[current_arg];
current_arg++;
continue;
}
if (arg == "-m") {
config.python_mode = true;
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the -m option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.python_module = argv[current_arg];
current_arg++;
// Collect remaining args as module args
while (current_arg < argc) {
config.script_args.push_back(argv[current_arg]);
current_arg++;
}
continue;
}
if (arg == "-i") {
config.interactive_mode = true;
config.python_mode = true;
current_arg++;
continue;
}
// McRogueFace specific flags
if (arg == "--headless") {
config.headless = true;
config.audio_enabled = false;
current_arg++;
continue;
}
if (arg == "--audio-off") {
config.audio_enabled = false;
current_arg++;
continue;
}
if (arg == "--audio-on") {
config.audio_enabled = true;
current_arg++;
continue;
}
if (arg == "--screenshot") {
config.take_screenshot = true;
current_arg++;
if (current_arg < argc && argv[current_arg][0] != '-') {
config.screenshot_path = argv[current_arg];
current_arg++;
} else {
config.screenshot_path = "screenshot.png";
}
continue;
}
if (arg == "--exec") {
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the --exec option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.exec_scripts.push_back(argv[current_arg]);
config.python_mode = true;
current_arg++;
continue;
}
if (arg == "--continue-after-exceptions") {
config.exit_on_exception = false;
current_arg++;
continue;
}
// If no flags matched, treat as positional argument (script name)
if (arg[0] != '-') {
config.script_path = arg;
config.python_mode = true;
current_arg++;
// Remaining args are script args
while (current_arg < argc) {
config.script_args.push_back(argv[current_arg]);
current_arg++;
}
break;
}
// Unknown flag
std::cerr << "Unknown option: " << arg << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
return result;
}
void CommandLineParser::print_help() {
std::cout << "usage: mcrogueface [option] ... [-c cmd | -m mod | file | -] [arg] ...\n"
<< "Options:\n"
<< " -c cmd : program passed in as string (terminates option list)\n"
<< " -h : print this help message and exit (also --help)\n"
<< " -i : inspect interactively after running script\n"
<< " -m mod : run library module as a script (terminates option list)\n"
<< " -V : print the Python version number and exit (also --version)\n"
<< "\n"
<< "McRogueFace specific options:\n"
<< " --exec file : execute script before main program (can be used multiple times)\n"
<< " --headless : run without creating a window (implies --audio-off)\n"
<< " --audio-off : disable audio\n"
<< " --audio-on : enable audio (even in headless mode)\n"
<< " --screenshot [path] : take a screenshot in headless mode\n"
<< " --continue-after-exceptions : don't exit on Python callback exceptions\n"
<< " (default: exit on first exception)\n"
<< "\n"
<< "Arguments:\n"
<< " file : program read from script file\n"
<< " - : program read from stdin\n"
<< " arg ...: arguments passed to program in sys.argv[1:]\n";
}
void CommandLineParser::print_version() {
std::cout << "Python 3.14.0 (McRogueFace embedded)\n";
}

View File

@ -1,30 +0,0 @@
#ifndef COMMAND_LINE_PARSER_H
#define COMMAND_LINE_PARSER_H
#include <string>
#include <vector>
#include "McRogueFaceConfig.h"
class CommandLineParser {
public:
struct ParseResult {
bool should_exit = false;
int exit_code = 0;
};
CommandLineParser(int argc, char* argv[]);
ParseResult parse(McRogueFaceConfig& config);
private:
int argc;
char** argv;
int current_arg = 1; // Skip program name
bool has_flag(const std::string& short_flag, const std::string& long_flag = "");
std::string get_next_arg(const std::string& flag_name);
void parse_positional_args(McRogueFaceConfig& config);
void print_help();
void print_version();
};
#endif // COMMAND_LINE_PARSER_H

View File

@ -4,357 +4,67 @@
#include "PyScene.h" #include "PyScene.h"
#include "UITestScene.h" #include "UITestScene.h"
#include "Resources.h" #include "Resources.h"
#include "Animation.h"
#include "Timer.h"
#include "BenchmarkLogger.h"
#include "imgui.h"
#include "imgui-SFML.h"
#include <cmath>
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{}) GameEngine::GameEngine()
{
}
GameEngine::GameEngine(const McRogueFaceConfig& cfg)
: config(cfg), headless(cfg.headless)
{ {
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf"); Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
Resources::game = this; Resources::game = this;
window_title = "McRogueFace Engine"; window_title = "McRogueFace - 7DRL 2024 Engine Demo";
window.create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close);
// Initialize rendering based on headless mode visible = window.getDefaultView();
if (headless) { window.setFramerateLimit(30);
headless_renderer = std::make_unique<HeadlessRenderer>();
if (!headless_renderer->init(1024, 768)) {
throw std::runtime_error("Failed to initialize headless renderer");
}
render_target = &headless_renderer->getRenderTarget();
} else {
window = std::make_unique<sf::RenderWindow>();
window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close | sf::Style::Resize);
window->setFramerateLimit(60);
render_target = window.get();
// Initialize ImGui for the window
if (ImGui::SFML::Init(*window)) {
imguiInitialized = true;
}
}
visible = render_target->getDefaultView();
// Initialize the game view
gameView.setSize(static_cast<float>(gameResolution.x), static_cast<float>(gameResolution.y));
// Use integer center coordinates for pixel-perfect rendering
gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f));
updateViewport();
scene = "uitest"; scene = "uitest";
scenes["uitest"] = new UITestScene(this); scenes["uitest"] = new UITestScene(this);
McRFPy_API::game = this; McRFPy_API::game = this;
// Initialize profiler overlay
profilerOverlay = new ProfilerOverlay(Resources::font);
// Only load game.py if no custom script/command/module/exec is specified
bool should_load_game = config.script_path.empty() &&
config.python_command.empty() &&
config.python_module.empty() &&
config.exec_scripts.empty() &&
!config.interactive_mode &&
!config.python_mode;
if (should_load_game) {
if (!Py_IsInitialized()) {
McRFPy_API::api_init(); McRFPy_API::api_init();
}
McRFPy_API::executePyString("import mcrfpy"); McRFPy_API::executePyString("import mcrfpy");
McRFPy_API::executeScript("scripts/game.py"); McRFPy_API::executeScript("scripts/game.py");
}
// Note: --exec scripts are NOT executed here.
// They are executed via executeStartupScripts() after the final engine is set up.
// This prevents double-execution when main.cpp creates multiple GameEngine instances.
clock.restart(); clock.restart();
runtime.restart(); runtime.restart();
} }
void GameEngine::executeStartupScripts()
{
// Execute any --exec scripts in order
// This is called ONCE from main.cpp after the final engine is set up
if (!config.exec_scripts.empty()) {
if (!Py_IsInitialized()) {
McRFPy_API::api_init();
}
McRFPy_API::executePyString("import mcrfpy");
for (const auto& exec_script : config.exec_scripts) {
std::cout << "Executing script: " << exec_script << std::endl;
McRFPy_API::executeScript(exec_script.string());
}
std::cout << "All --exec scripts completed" << std::endl;
}
}
GameEngine::~GameEngine()
{
cleanup();
for (auto& [name, scene] : scenes) {
delete scene;
}
delete profilerOverlay;
}
void GameEngine::cleanup()
{
if (cleaned_up) return;
cleaned_up = true;
// Clear all animations first (RAII handles invalidation)
AnimationManager::getInstance().clear();
// Clear Python references before destroying C++ objects
// Clear all timers (they hold Python callables)
timers.clear();
// Clear McRFPy_API's reference to this game engine
if (McRFPy_API::game == this) {
McRFPy_API::game = nullptr;
}
// Shutdown ImGui before closing window
if (imguiInitialized) {
ImGui::SFML::Shutdown();
imguiInitialized = false;
}
// Force close the window if it's still open
if (window && window->isOpen()) {
window->close();
}
}
Scene* GameEngine::currentScene() { return scenes[scene]; } Scene* GameEngine::currentScene() { return scenes[scene]; }
Scene* GameEngine::getScene(const std::string& name) {
auto it = scenes.find(name);
return (it != scenes.end()) ? it->second : nullptr;
}
void GameEngine::changeScene(std::string s) void GameEngine::changeScene(std::string s)
{ {
changeScene(s, TransitionType::None, 0.0f); /*std::cout << "Current scene is now '" << s << "'\n";*/
} if (scenes.find(s) != scenes.end())
scene = s;
void GameEngine::changeScene(std::string sceneName, TransitionType transitionType, float duration)
{
if (scenes.find(sceneName) == scenes.end())
{
std::cout << "Attempted to change to a scene that doesn't exist (`" << sceneName << "`)" << std::endl;
return;
}
if (transitionType == TransitionType::None || duration <= 0.0f)
{
// Immediate scene change
std::string old_scene = scene;
scene = sceneName;
// Trigger Python scene lifecycle events
McRFPy_API::triggerSceneChange(old_scene, sceneName);
}
else else
{ std::cout << "Attempted to change to a scene that doesn't exist (`" << s << "`)" << std::endl;
// Start transition
transition.start(transitionType, scene, sceneName, duration);
// Render current scene to texture
sf::RenderTarget* original_target = render_target;
render_target = transition.oldSceneTexture.get();
transition.oldSceneTexture->clear();
currentScene()->render();
transition.oldSceneTexture->display();
// Change to new scene
std::string old_scene = scene;
scene = sceneName;
// Render new scene to texture
render_target = transition.newSceneTexture.get();
transition.newSceneTexture->clear();
currentScene()->render();
transition.newSceneTexture->display();
// Restore original render target and scene
render_target = original_target;
scene = old_scene;
}
} }
void GameEngine::quit() { running = false; } void GameEngine::quit() { running = false; }
void GameEngine::setPause(bool p) { paused = p; } void GameEngine::setPause(bool p) { paused = p; }
sf::Font & GameEngine::getFont() { /*return font; */ return Resources::font; } sf::Font & GameEngine::getFont() { /*return font; */ return Resources::font; }
sf::RenderWindow & GameEngine::getWindow() { sf::RenderWindow & GameEngine::getWindow() { return window; }
if (!window) {
throw std::runtime_error("Window not available in headless mode");
}
return *window;
}
sf::RenderTarget & GameEngine::getRenderTarget() {
return *render_target;
}
void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); } void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); }
void GameEngine::setWindowScale(float multiplier) void GameEngine::setWindowScale(float multiplier)
{ {
if (!headless && window) { window.setSize(sf::Vector2u(1024 * multiplier, 768 * multiplier)); // 7DRL 2024: window scaling
window->setSize(sf::Vector2u(gameResolution.x * multiplier, gameResolution.y * multiplier)); //window.create(sf::VideoMode(1024 * multiplier, 768 * multiplier), window_title, sf::Style::Titlebar | sf::Style::Close);
updateViewport();
}
} }
void GameEngine::run() void GameEngine::run()
{ {
//std::cout << "GameEngine::run() starting main loop..." << std::endl;
float fps = 0.0; float fps = 0.0;
frameTime = 0.016f; // Initialize to ~60 FPS
clock.restart(); clock.restart();
while (running) while (running)
{ {
// Reset per-frame metrics
metrics.resetPerFrame();
currentScene()->update(); currentScene()->update();
testTimers(); testTimers();
// Update Python scenes
{
ScopedTimer pyTimer(metrics.pythonScriptTime);
McRFPy_API::updatePythonScenes(frameTime);
}
// Update animations (only if frameTime is valid)
if (frameTime > 0.0f && frameTime < 1.0f) {
ScopedTimer animTimer(metrics.animationTime);
AnimationManager::getInstance().update(frameTime);
}
if (!headless) {
sUserInput(); sUserInput();
// Update ImGui
if (imguiInitialized) {
ImGui::SFML::Update(*window, clock.getElapsedTime());
}
}
if (!paused) if (!paused)
{ {
} }
// Handle scene transitions
if (transition.type != TransitionType::None)
{
transition.update(frameTime);
if (transition.isComplete())
{
// Transition complete - finalize scene change
scene = transition.toScene;
transition.type = TransitionType::None;
// Trigger Python scene lifecycle events
McRFPy_API::triggerSceneChange(transition.fromScene, transition.toScene);
}
else
{
// Render transition
render_target->clear();
transition.render(*render_target);
}
}
else
{
// Normal scene rendering
currentScene()->render(); currentScene()->render();
}
// Update and render profiler overlay (if enabled)
if (profilerOverlay && !headless) {
profilerOverlay->update(metrics);
profilerOverlay->render(*render_target);
}
// Render ImGui console overlay
if (imguiInitialized && !headless) {
console.render();
ImGui::SFML::Render(*window);
}
// Record work time before display (which may block for vsync/framerate limit)
metrics.workTime = clock.getElapsedTime().asSeconds() * 1000.0f;
// Display the frame
if (headless) {
headless_renderer->display();
// Take screenshot if requested
if (config.take_screenshot) {
headless_renderer->saveScreenshot(config.screenshot_path.empty() ? "screenshot.png" : config.screenshot_path);
config.take_screenshot = false; // Only take one screenshot
}
} else {
window->display();
}
currentFrame++; currentFrame++;
frameTime = clock.restart().asSeconds(); frameTime = clock.restart().asSeconds();
fps = 1 / frameTime; fps = 1 / frameTime;
window.setTitle(window_title + " " + std::to_string(fps) + " FPS");
// Update profiling metrics
metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds
// Record frame data for benchmark logging (if running)
g_benchmarkLogger.recordFrame(metrics);
int whole_fps = metrics.fps;
int tenth_fps = (metrics.fps * 10) % 10;
if (!headless && window) {
window->setTitle(window_title);
} }
// In windowed mode, check if window was closed
if (!headless && window && !window->isOpen()) {
running = false;
}
// In headless exec mode, auto-exit when no timers remain
if (config.auto_exit_after_exec && timers.empty()) {
running = false;
}
// Check if a Python exception has signaled exit
if (McRFPy_API::shouldExit()) {
running = false;
}
}
// Clean up before exiting the run loop
cleanup();
// #144: Quick exit to avoid cleanup segfaults in Python/C++ destructor ordering
// This is a pragmatic workaround - proper cleanup would require careful
// attention to shared_ptr cycles and Python GC interaction
std::_Exit(0);
}
std::shared_ptr<Timer> GameEngine::getTimer(const std::string& name)
{
auto it = timers.find(name);
if (it != timers.end()) {
return it->second;
}
return nullptr;
} }
void GameEngine::manageTimer(std::string name, PyObject* target, int interval) void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
@ -366,7 +76,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
{ {
// Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check // Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check
// see gitea issue #4: this allows for a timer to be deleted during its own call to itself // see gitea issue #4: this allows for a timer to be deleted during its own call to itself
timers[name] = std::make_shared<Timer>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds()); timers[name] = std::make_shared<PyTimerCallable>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds());
return; return;
} }
} }
@ -375,7 +85,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl; std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl;
return; return;
} }
timers[name] = std::make_shared<Timer>(target, interval, runtime.getElapsedTime().asMilliseconds()); timers[name] = std::make_shared<PyTimerCallable>(target, interval, runtime.getElapsedTime().asMilliseconds());
} }
void GameEngine::testTimers() void GameEngine::testTimers()
@ -384,15 +94,9 @@ void GameEngine::testTimers()
auto it = timers.begin(); auto it = timers.begin();
while (it != timers.end()) while (it != timers.end())
{ {
// Keep a local copy of the timer to prevent use-after-free. it->second->test(now);
// If the callback calls delTimer(), the map entry gets replaced,
// but we need the Timer object to survive until test() returns.
auto timer = it->second;
timer->test(now);
// Remove timers that have been cancelled or are one-shot and fired. if (it->second->isNone())
// Note: Check it->second (current map value) in case callback replaced it.
if (!it->second->getCallback() || it->second->getCallback() == Py_None)
{ {
it = timers.erase(it); it = timers.erase(it);
} }
@ -401,27 +105,29 @@ void GameEngine::testTimers()
} }
} }
void GameEngine::processEvent(const sf::Event& event) void GameEngine::sUserInput()
{ {
sf::Event event;
while (window.pollEvent(event))
{
std::string actionType; std::string actionType;
int actionCode = 0; int actionCode = 0;
if (event.type == sf::Event::Closed) { running = false; return; } if (event.type == sf::Event::Closed) { running = false; continue; }
// TODO: add resize event to Scene to react; call it after constructor too, maybe
// Handle F3 for profiler overlay toggle
if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F3) {
if (profilerOverlay) {
profilerOverlay->toggle();
}
return;
}
// Handle window resize events
else if (event.type == sf::Event::Resized) { else if (event.type == sf::Event::Resized) {
// Update the viewport to handle the new window size continue; // 7DRL short circuit. Resizing manually disabled
updateViewport(); /*
sf::FloatRect area(0.f, 0.f, event.size.width, event.size.height);
// Notify Python scenes about the resize //sf::FloatRect area(0.f, 0.f, 1024.f, 768.f); // 7DRL 2024: attempt to set scale appropriately
McRFPy_API::triggerResize(event.size.width, event.size.height); //sf::FloatRect area(0.f, 0.f, event.size.width, event.size.width * 0.75);
visible = sf::View(area);
window.setView(visible);
//window.setSize(sf::Vector2u(event.size.width, event.size.width * 0.75)); // 7DRL 2024: window scaling
std::cout << "Visible area set to (0, 0, " << event.size.width << ", " << event.size.height <<")"<<std::endl;
actionType = "resize";
//window.setSize(sf::Vector2u(event.size.width, event.size.width * 0.75)); // 7DRL 2024: window scaling
*/
} }
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start"; else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start";
@ -433,64 +139,53 @@ void GameEngine::processEvent(const sf::Event& event)
actionCode = ActionCode::keycode(event.key.code); actionCode = ActionCode::keycode(event.key.code);
else if (event.type == sf::Event::MouseWheelScrolled) else if (event.type == sf::Event::MouseWheelScrolled)
{ {
// //sf::Mouse::Wheel w = event.MouseWheelScrollEvent.wheel;
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel) if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
{ {
int delta = 1; int delta = 1;
if (event.mouseWheelScroll.delta < 0) delta = -1; if (event.mouseWheelScroll.delta < 0) delta = -1;
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta ); actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
/*
std::cout << "[GameEngine] Generated MouseWheel code w(" << (int)event.mouseWheelScroll.wheel << ") d(" << event.mouseWheelScroll.delta << ") D(" << delta << ") = " << actionCode << std::endl;
std::cout << " test decode: isMouseWheel=" << ActionCode::isMouseWheel(actionCode) << ", wheel=" << ActionCode::wheel(actionCode) << ", delta=" << ActionCode::delta(actionCode) << std::endl;
std::cout << " math test: actionCode && WHEEL_NEG -> " << (actionCode && ActionCode::WHEEL_NEG) << "; actionCode && WHEEL_DEL -> " << (actionCode && ActionCode::WHEEL_DEL) << ";" << std::endl;
*/
} }
} // float d = event.MouseWheelScrollEvent.delta;
// #140 - Handle mouse movement for hover detection // actionCode = ActionCode::keycode(0, d);
else if (event.type == sf::Event::MouseMoved)
{
// Cast to PyScene to call do_mouse_hover
if (auto* pyscene = dynamic_cast<PyScene*>(currentScene())) {
pyscene->do_mouse_hover(event.mouseMove.x, event.mouseMove.y);
}
return;
} }
else else
return; continue;
//std::cout << "Event produced action code " << actionCode << ": " << actionType << std::endl;
if (currentScene()->hasAction(actionCode)) if (currentScene()->hasAction(actionCode))
{ {
std::string name = currentScene()->action(actionCode); std::string name = currentScene()->action(actionCode);
currentScene()->doAction(name, actionType); currentScene()->doAction(name, actionType);
} }
else if (currentScene()->key_callable && else if (currentScene()->key_callable)
(event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased))
{ {
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType); currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
} /*
} PyObject* args = Py_BuildValue("(ss)", ActionCode::key_str(event.key.code).c_str(), actionType.c_str());
PyObject* retval = PyObject_Call(currentScene()->key_callable, args, NULL);
void GameEngine::sUserInput() if (!retval)
{
sf::Event event;
while (window && window->pollEvent(event))
{ {
// Process event through ImGui first std::cout << "key_callable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
if (imguiInitialized) { PyErr_Print();
ImGui::SFML::ProcessEvent(*window, event); PyErr_Clear();
} else if (retval != Py_None)
{
std::cout << "key_callable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
} }
*/
// Handle grave/tilde key for console toggle (before other processing)
if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Grave) {
console.toggle();
continue; // Don't pass grave key to game
} }
else
// If console wants keyboard, don't pass keyboard events to game {
if (console.wantsKeyboardInput()) { //std::cout << "[GameEngine] Action not registered for input: " << actionCode << ": " << actionType << std::endl;
// Still process non-keyboard events (mouse, window close, etc.)
if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased ||
event.type == sf::Event::TextEntered) {
continue;
} }
} }
processEvent(event);
}
} }
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> GameEngine::scene_ui(std::string target) std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> GameEngine::scene_ui(std::string target)
@ -510,123 +205,3 @@ std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> GameEngine::scene_ui(s
if (scenes.count(target) == 0) return NULL; if (scenes.count(target) == 0) return NULL;
return scenes[target]->ui_elements; return scenes[target]->ui_elements;
} }
void GameEngine::setWindowTitle(const std::string& title)
{
window_title = title;
if (!headless && window) {
window->setTitle(title);
}
}
void GameEngine::setVSync(bool enabled)
{
vsync_enabled = enabled;
if (!headless && window) {
window->setVerticalSyncEnabled(enabled);
}
}
void GameEngine::setFramerateLimit(unsigned int limit)
{
framerate_limit = limit;
if (!headless && window) {
window->setFramerateLimit(limit);
}
}
void GameEngine::setGameResolution(unsigned int width, unsigned int height) {
gameResolution = sf::Vector2u(width, height);
gameView.setSize(static_cast<float>(width), static_cast<float>(height));
// Use integer center coordinates for pixel-perfect rendering
gameView.setCenter(std::floor(width / 2.0f), std::floor(height / 2.0f));
updateViewport();
}
void GameEngine::setViewportMode(ViewportMode mode) {
viewportMode = mode;
updateViewport();
}
std::string GameEngine::getViewportModeString() const {
switch (viewportMode) {
case ViewportMode::Center: return "center";
case ViewportMode::Stretch: return "stretch";
case ViewportMode::Fit: return "fit";
}
return "unknown";
}
void GameEngine::updateViewport() {
if (!render_target) return;
auto windowSize = render_target->getSize();
switch (viewportMode) {
case ViewportMode::Center: {
// 1:1 pixels, centered in window
float viewportWidth = std::min(static_cast<float>(gameResolution.x), static_cast<float>(windowSize.x));
float viewportHeight = std::min(static_cast<float>(gameResolution.y), static_cast<float>(windowSize.y));
// Floor offsets to ensure integer pixel alignment
float offsetX = std::floor((windowSize.x - viewportWidth) / 2.0f);
float offsetY = std::floor((windowSize.y - viewportHeight) / 2.0f);
gameView.setViewport(sf::FloatRect(
offsetX / windowSize.x,
offsetY / windowSize.y,
viewportWidth / windowSize.x,
viewportHeight / windowSize.y
));
break;
}
case ViewportMode::Stretch: {
// Fill entire window, ignore aspect ratio
gameView.setViewport(sf::FloatRect(0, 0, 1, 1));
break;
}
case ViewportMode::Fit: {
// Maintain aspect ratio with black bars
float windowAspect = static_cast<float>(windowSize.x) / windowSize.y;
float gameAspect = static_cast<float>(gameResolution.x) / gameResolution.y;
float viewportWidth, viewportHeight;
float offsetX = 0, offsetY = 0;
if (windowAspect > gameAspect) {
// Window is wider - black bars on sides
// Calculate viewport size in pixels and floor for pixel-perfect scaling
float pixelHeight = static_cast<float>(windowSize.y);
float pixelWidth = std::floor(pixelHeight * gameAspect);
viewportHeight = 1.0f;
viewportWidth = pixelWidth / windowSize.x;
offsetX = (1.0f - viewportWidth) / 2.0f;
} else {
// Window is taller - black bars on top/bottom
// Calculate viewport size in pixels and floor for pixel-perfect scaling
float pixelWidth = static_cast<float>(windowSize.x);
float pixelHeight = std::floor(pixelWidth / gameAspect);
viewportWidth = 1.0f;
viewportHeight = pixelHeight / windowSize.y;
offsetY = (1.0f - viewportHeight) / 2.0f;
}
gameView.setViewport(sf::FloatRect(offsetX, offsetY, viewportWidth, viewportHeight));
break;
}
}
// Apply the view
render_target->setView(gameView);
}
sf::Vector2f GameEngine::windowToGameCoords(const sf::Vector2f& windowPos) const {
if (!render_target) return windowPos;
// Convert window coordinates to game coordinates using the view
return render_target->mapPixelToCoords(sf::Vector2i(windowPos), gameView);
}

View File

@ -6,97 +6,10 @@
#include "IndexTexture.h" #include "IndexTexture.h"
#include "Timer.h" #include "Timer.h"
#include "PyCallable.h" #include "PyCallable.h"
#include "McRogueFaceConfig.h"
#include "HeadlessRenderer.h"
#include "SceneTransition.h"
#include "Profiler.h"
#include "ImGuiConsole.h"
#include <memory>
#include <sstream>
/**
* @brief Performance profiling metrics structure
*
* Tracks frame timing, render counts, and detailed timing breakdowns.
* Used by GameEngine, ProfilerOverlay (F3), and BenchmarkLogger.
*/
struct ProfilingMetrics {
float frameTime = 0.0f; // Current frame time in milliseconds
float avgFrameTime = 0.0f; // Average frame time over last N frames
int fps = 0; // Frames per second
int drawCalls = 0; // Draw calls per frame
int uiElements = 0; // Number of UI elements rendered
int visibleElements = 0; // Number of visible elements
// Detailed timing breakdowns (added for profiling system)
float gridRenderTime = 0.0f; // Time spent rendering grids (ms)
float entityRenderTime = 0.0f; // Time spent rendering entities (ms)
float fovOverlayTime = 0.0f; // Time spent rendering FOV overlays (ms)
float pythonScriptTime = 0.0f; // Time spent in Python callbacks (ms)
float animationTime = 0.0f; // Time spent updating animations (ms)
float workTime = 0.0f; // Total work time before display/sleep (ms)
// Grid-specific metrics
int gridCellsRendered = 0; // Number of grid cells drawn this frame
int entitiesRendered = 0; // Number of entities drawn this frame
int totalEntities = 0; // Total entities in scene
// Frame time history for averaging
static constexpr int HISTORY_SIZE = 60;
float frameTimeHistory[HISTORY_SIZE] = {0};
int historyIndex = 0;
void updateFrameTime(float deltaMs) {
frameTime = deltaMs;
frameTimeHistory[historyIndex] = deltaMs;
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
// Calculate average
float sum = 0.0f;
for (int i = 0; i < HISTORY_SIZE; ++i) {
sum += frameTimeHistory[i];
}
avgFrameTime = sum / HISTORY_SIZE;
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
}
void resetPerFrame() {
drawCalls = 0;
uiElements = 0;
visibleElements = 0;
// Reset per-frame timing metrics
gridRenderTime = 0.0f;
entityRenderTime = 0.0f;
fovOverlayTime = 0.0f;
pythonScriptTime = 0.0f;
animationTime = 0.0f;
// Reset per-frame counters
gridCellsRendered = 0;
entitiesRendered = 0;
totalEntities = 0;
}
};
class GameEngine class GameEngine
{ {
public: sf::RenderWindow window;
// Forward declare nested class so private section can use it
class ProfilerOverlay;
// Viewport modes (moved here so private section can use it)
enum class ViewportMode {
Center, // 1:1 pixels, viewport centered in window
Stretch, // viewport size = window size, doesn't respect aspect ratio
Fit // maintains original aspect ratio, leaves black bars
};
private:
std::unique_ptr<sf::RenderWindow> window;
std::unique_ptr<HeadlessRenderer> headless_renderer;
sf::RenderTarget* render_target;
sf::Font font; sf::Font font;
std::map<std::string, Scene*> scenes; std::map<std::string, Scene*> scenes;
bool running = true; bool running = true;
@ -107,87 +20,28 @@ private:
float frameTime; float frameTime;
std::string window_title; std::string window_title;
bool headless = false; sf::Clock runtime;
McRogueFaceConfig config; //std::map<std::string, Timer> timers;
bool cleaned_up = false; std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
// Window state tracking
bool vsync_enabled = false;
unsigned int framerate_limit = 60;
// Scene transition state
SceneTransition transition;
// Viewport system
sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution
sf::View gameView; // View for the game content
ViewportMode viewportMode = ViewportMode::Fit;
// Profiling overlay
bool showProfilerOverlay = false; // F3 key toggles this
int overlayUpdateCounter = 0; // Only update overlay every N frames
ProfilerOverlay* profilerOverlay = nullptr; // The actual overlay renderer
// ImGui console overlay
ImGuiConsole console;
bool imguiInitialized = false;
void updateViewport();
void testTimers(); void testTimers();
public: public:
sf::Clock runtime;
std::map<std::string, std::shared_ptr<Timer>> timers;
std::string scene; std::string scene;
// Profiling metrics (struct defined above class)
ProfilingMetrics metrics;
GameEngine(); GameEngine();
GameEngine(const McRogueFaceConfig& cfg);
~GameEngine();
Scene* currentScene(); Scene* currentScene();
Scene* getScene(const std::string& name); // #118: Get scene by name
void changeScene(std::string); void changeScene(std::string);
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
void createScene(std::string); void createScene(std::string);
void quit(); void quit();
void setPause(bool); void setPause(bool);
sf::Font & getFont(); sf::Font & getFont();
sf::RenderWindow & getWindow(); sf::RenderWindow & getWindow();
sf::RenderTarget & getRenderTarget();
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
void run(); void run();
void sUserInput(); void sUserInput();
void cleanup(); // Clean up Python references before destruction
void executeStartupScripts(); // Execute --exec scripts (called once after final engine setup)
int getFrame() { return currentFrame; } int getFrame() { return currentFrame; }
float getFrameTime() { return frameTime; } float getFrameTime() { return frameTime; }
sf::View getView() { return visible; } sf::View getView() { return visible; }
void manageTimer(std::string, PyObject*, int); void manageTimer(std::string, PyObject*, int);
std::shared_ptr<Timer> getTimer(const std::string& name);
void setWindowScale(float); void setWindowScale(float);
bool isHeadless() const { return headless; }
const McRogueFaceConfig& getConfig() const { return config; }
void setAutoExitAfterExec(bool enabled) { config.auto_exit_after_exec = enabled; }
void processEvent(const sf::Event& event);
// Window property accessors
const std::string& getWindowTitle() const { return window_title; }
void setWindowTitle(const std::string& title);
bool getVSync() const { return vsync_enabled; }
void setVSync(bool enabled);
unsigned int getFramerateLimit() const { return framerate_limit; }
void setFramerateLimit(unsigned int limit);
// Viewport system
void setGameResolution(unsigned int width, unsigned int height);
sf::Vector2u getGameResolution() const { return gameResolution; }
void setViewportMode(ViewportMode mode);
ViewportMode getViewportMode() const { return viewportMode; }
std::string getViewportModeString() const;
sf::Vector2f windowToGameCoords(const sf::Vector2f& windowPos) const;
// global textures for scripts to access // global textures for scripts to access
std::vector<IndexTexture> textures; std::vector<IndexTexture> textures;
@ -199,28 +53,3 @@ public:
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> scene_ui(std::string scene); std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> scene_ui(std::string scene);
}; };
/**
* @brief Visual overlay that displays real-time profiling metrics
*/
class GameEngine::ProfilerOverlay {
private:
sf::Font& font;
sf::Text text;
sf::RectangleShape background;
bool visible;
int updateInterval;
int frameCounter;
sf::Color getPerformanceColor(float frameTimeMs);
std::string formatFloat(float value, int precision = 1);
std::string formatPercentage(float part, float total);
public:
ProfilerOverlay(sf::Font& fontRef);
void toggle();
void setVisible(bool vis);
bool isVisible() const;
void update(const ProfilingMetrics& metrics);
void render(sf::RenderTarget& target);
};

View File

@ -1,201 +0,0 @@
#include "GridChunk.h"
#include "UIGrid.h"
#include "PyTexture.h"
#include <algorithm>
#include <cmath>
// =============================================================================
// GridChunk implementation
// =============================================================================
GridChunk::GridChunk(int chunk_x, int chunk_y, int width, int height,
int world_x, int world_y, UIGrid* parent)
: chunk_x(chunk_x), chunk_y(chunk_y),
width(width), height(height),
world_x(world_x), world_y(world_y),
cells(width * height),
dirty(true),
parent_grid(parent)
{}
UIGridPoint& GridChunk::at(int local_x, int local_y) {
return cells[local_y * width + local_x];
}
const UIGridPoint& GridChunk::at(int local_x, int local_y) const {
return cells[local_y * width + local_x];
}
void GridChunk::markDirty() {
dirty = true;
}
// #150 - Removed ensureTexture/renderToTexture - base layer rendering removed
// GridChunk now only provides data storage for GridPoints
sf::FloatRect GridChunk::getWorldBounds(int cell_width, int cell_height) const {
return sf::FloatRect(
sf::Vector2f(world_x * cell_width, world_y * cell_height),
sf::Vector2f(width * cell_width, height * cell_height)
);
}
bool GridChunk::isVisible(float left_edge, float top_edge,
float right_edge, float bottom_edge) const {
// Check if chunk's cell range overlaps with viewport's cell range
float chunk_right = world_x + width;
float chunk_bottom = world_y + height;
return !(world_x >= right_edge || chunk_right <= left_edge ||
world_y >= bottom_edge || chunk_bottom <= top_edge);
}
// =============================================================================
// ChunkManager implementation
// =============================================================================
ChunkManager::ChunkManager(int grid_x, int grid_y, UIGrid* parent)
: grid_x(grid_x), grid_y(grid_y), parent_grid(parent)
{
// Calculate number of chunks needed
chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
chunks.reserve(chunks_x * chunks_y);
// Create chunks
for (int cy = 0; cy < chunks_y; ++cy) {
for (int cx = 0; cx < chunks_x; ++cx) {
// Calculate world position
int world_x = cx * GridChunk::CHUNK_SIZE;
int world_y = cy * GridChunk::CHUNK_SIZE;
// Calculate actual size (may be smaller at edges)
int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x);
int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y);
chunks.push_back(std::make_unique<GridChunk>(
cx, cy, chunk_width, chunk_height, world_x, world_y, parent
));
}
}
}
GridChunk* ChunkManager::getChunkForCell(int x, int y) {
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
return nullptr;
}
int chunk_x = x / GridChunk::CHUNK_SIZE;
int chunk_y = y / GridChunk::CHUNK_SIZE;
return getChunk(chunk_x, chunk_y);
}
const GridChunk* ChunkManager::getChunkForCell(int x, int y) const {
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
return nullptr;
}
int chunk_x = x / GridChunk::CHUNK_SIZE;
int chunk_y = y / GridChunk::CHUNK_SIZE;
return getChunk(chunk_x, chunk_y);
}
GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) {
if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) {
return nullptr;
}
return chunks[chunk_y * chunks_x + chunk_x].get();
}
const GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) const {
if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) {
return nullptr;
}
return chunks[chunk_y * chunks_x + chunk_x].get();
}
UIGridPoint& ChunkManager::at(int x, int y) {
GridChunk* chunk = getChunkForCell(x, y);
if (!chunk) {
// Return a static dummy point for out-of-bounds access
// This matches the original behavior of UIGrid::at()
static UIGridPoint dummy;
return dummy;
}
// Convert to local coordinates within chunk
int local_x = x % GridChunk::CHUNK_SIZE;
int local_y = y % GridChunk::CHUNK_SIZE;
// Mark chunk dirty when accessed for modification
chunk->markDirty();
return chunk->at(local_x, local_y);
}
const UIGridPoint& ChunkManager::at(int x, int y) const {
const GridChunk* chunk = getChunkForCell(x, y);
if (!chunk) {
static UIGridPoint dummy;
return dummy;
}
int local_x = x % GridChunk::CHUNK_SIZE;
int local_y = y % GridChunk::CHUNK_SIZE;
return chunk->at(local_x, local_y);
}
void ChunkManager::markAllDirty() {
for (auto& chunk : chunks) {
chunk->markDirty();
}
}
std::vector<GridChunk*> ChunkManager::getVisibleChunks(float left_edge, float top_edge,
float right_edge, float bottom_edge) {
std::vector<GridChunk*> visible;
visible.reserve(chunks.size()); // Pre-allocate for worst case
for (auto& chunk : chunks) {
if (chunk->isVisible(left_edge, top_edge, right_edge, bottom_edge)) {
visible.push_back(chunk.get());
}
}
return visible;
}
void ChunkManager::resize(int new_grid_x, int new_grid_y) {
// For now, simple rebuild - could be optimized to preserve data
grid_x = new_grid_x;
grid_y = new_grid_y;
chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
chunks.clear();
chunks.reserve(chunks_x * chunks_y);
for (int cy = 0; cy < chunks_y; ++cy) {
for (int cx = 0; cx < chunks_x; ++cx) {
int world_x = cx * GridChunk::CHUNK_SIZE;
int world_y = cy * GridChunk::CHUNK_SIZE;
int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x);
int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y);
chunks.push_back(std::make_unique<GridChunk>(
cx, cy, chunk_width, chunk_height, world_x, world_y, parent_grid
));
}
}
}
int ChunkManager::dirtyChunks() const {
int count = 0;
for (const auto& chunk : chunks) {
if (chunk->dirty) ++count;
}
return count;
}

View File

@ -1,108 +0,0 @@
#pragma once
#include "Common.h"
#include <SFML/Graphics.hpp>
#include <vector>
#include <memory>
#include "UIGridPoint.h"
// Forward declarations
class UIGrid;
class PyTexture;
/**
* #123 - Grid chunk for sub-grid data storage
* #150 - Rendering removed; layers now handle all rendering
*
* Each chunk represents a CHUNK_SIZE x CHUNK_SIZE portion of the grid.
* Chunks store GridPoint data for pathfinding and game logic.
*/
class GridChunk {
public:
// Compile-time configurable chunk size (power of 2 recommended)
static constexpr int CHUNK_SIZE = 64;
// Position of this chunk in chunk coordinates
int chunk_x, chunk_y;
// Actual dimensions (may be less than CHUNK_SIZE at grid edges)
int width, height;
// World position (in cell coordinates)
int world_x, world_y;
// Cell data for this chunk (pathfinding properties only)
std::vector<UIGridPoint> cells;
// Dirty flag (for layer sync if needed)
bool dirty;
// Parent grid reference
UIGrid* parent_grid;
// Constructor
GridChunk(int chunk_x, int chunk_y, int width, int height,
int world_x, int world_y, UIGrid* parent);
// Access cell at local chunk coordinates
UIGridPoint& at(int local_x, int local_y);
const UIGridPoint& at(int local_x, int local_y) const;
// Mark chunk as dirty
void markDirty();
// Get pixel bounds of this chunk in world coordinates
sf::FloatRect getWorldBounds(int cell_width, int cell_height) const;
// Check if chunk overlaps with viewport
bool isVisible(float left_edge, float top_edge,
float right_edge, float bottom_edge) const;
};
/**
* Manages a 2D array of chunks for a grid
*/
class ChunkManager {
public:
// Dimensions in chunks
int chunks_x, chunks_y;
// Grid dimensions in cells
int grid_x, grid_y;
// All chunks (row-major order)
std::vector<std::unique_ptr<GridChunk>> chunks;
// Parent grid
UIGrid* parent_grid;
// Constructor - creates chunks for given grid dimensions
ChunkManager(int grid_x, int grid_y, UIGrid* parent);
// Get chunk containing cell (x, y)
GridChunk* getChunkForCell(int x, int y);
const GridChunk* getChunkForCell(int x, int y) const;
// Get chunk at chunk coordinates
GridChunk* getChunk(int chunk_x, int chunk_y);
const GridChunk* getChunk(int chunk_x, int chunk_y) const;
// Access cell at grid coordinates (routes through chunk)
UIGridPoint& at(int x, int y);
const UIGridPoint& at(int x, int y) const;
// Mark all chunks dirty (for full rebuild)
void markAllDirty();
// Get chunks that overlap with viewport
std::vector<GridChunk*> getVisibleChunks(float left_edge, float top_edge,
float right_edge, float bottom_edge);
// Resize grid (rebuilds chunks)
void resize(int new_grid_x, int new_grid_y);
// Get total number of chunks
int totalChunks() const { return chunks_x * chunks_y; }
// Get number of dirty chunks
int dirtyChunks() const;
};

View File

@ -1,794 +0,0 @@
#include "GridLayers.h"
#include "UIGrid.h"
#include "PyColor.h"
#include "PyTexture.h"
#include <sstream>
// =============================================================================
// GridLayer base class
// =============================================================================
GridLayer::GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent)
: type(type), z_index(z_index), grid_x(grid_x), grid_y(grid_y),
parent_grid(parent), visible(true),
dirty(true), texture_initialized(false),
cached_cell_width(0), cached_cell_height(0)
{}
void GridLayer::markDirty() {
dirty = true;
}
void GridLayer::ensureTextureSize(int cell_width, int cell_height) {
// Check if we need to resize/create the texture
unsigned int required_width = grid_x * cell_width;
unsigned int required_height = grid_y * cell_height;
// Maximum texture size limit (prevent excessive memory usage)
const unsigned int MAX_TEXTURE_SIZE = 4096;
if (required_width > MAX_TEXTURE_SIZE) required_width = MAX_TEXTURE_SIZE;
if (required_height > MAX_TEXTURE_SIZE) required_height = MAX_TEXTURE_SIZE;
// Skip if already properly sized
if (texture_initialized &&
cached_texture.getSize().x == required_width &&
cached_texture.getSize().y == required_height &&
cached_cell_width == cell_width &&
cached_cell_height == cell_height) {
return;
}
// Create or resize the texture (SFML uses .create() not .resize())
if (!cached_texture.create(required_width, required_height)) {
// Creation failed - texture will remain uninitialized
texture_initialized = false;
return;
}
cached_cell_width = cell_width;
cached_cell_height = cell_height;
texture_initialized = true;
dirty = true; // Force re-render after resize
// Setup the sprite to use the texture
cached_sprite.setTexture(cached_texture.getTexture());
}
// =============================================================================
// ColorLayer implementation
// =============================================================================
ColorLayer::ColorLayer(int z_index, int grid_x, int grid_y, UIGrid* parent)
: GridLayer(GridLayerType::Color, z_index, grid_x, grid_y, parent),
colors(grid_x * grid_y, sf::Color::Transparent)
{}
sf::Color& ColorLayer::at(int x, int y) {
return colors[y * grid_x + x];
}
const sf::Color& ColorLayer::at(int x, int y) const {
return colors[y * grid_x + x];
}
void ColorLayer::fill(const sf::Color& color) {
std::fill(colors.begin(), colors.end(), color);
markDirty(); // #148 - Mark for re-render
}
void ColorLayer::resize(int new_grid_x, int new_grid_y) {
std::vector<sf::Color> new_colors(new_grid_x * new_grid_y, sf::Color::Transparent);
// Copy existing data
int copy_x = std::min(grid_x, new_grid_x);
int copy_y = std::min(grid_y, new_grid_y);
for (int y = 0; y < copy_y; ++y) {
for (int x = 0; x < copy_x; ++x) {
new_colors[y * new_grid_x + x] = colors[y * grid_x + x];
}
}
colors = std::move(new_colors);
grid_x = new_grid_x;
grid_y = new_grid_y;
// #148 - Invalidate cached texture (will be resized on next render)
texture_initialized = false;
markDirty();
}
// #148 - Render all cells to cached texture (called when dirty)
void ColorLayer::renderToTexture(int cell_width, int cell_height) {
ensureTextureSize(cell_width, cell_height);
if (!texture_initialized) return;
cached_texture.clear(sf::Color::Transparent);
sf::RectangleShape rect;
rect.setSize(sf::Vector2f(cell_width, cell_height));
rect.setOutlineThickness(0);
// Render all cells to cached texture (no zoom - 1:1 pixel mapping)
for (int x = 0; x < grid_x; ++x) {
for (int y = 0; y < grid_y; ++y) {
const sf::Color& color = at(x, y);
if (color.a == 0) continue; // Skip fully transparent
rect.setPosition(sf::Vector2f(x * cell_width, y * cell_height));
rect.setFillColor(color);
cached_texture.draw(rect);
}
}
cached_texture.display();
dirty = false;
}
void ColorLayer::render(sf::RenderTarget& target,
float left_spritepixels, float top_spritepixels,
int left_edge, int top_edge, int x_limit, int y_limit,
float zoom, int cell_width, int cell_height) {
if (!visible) return;
// #148 - Use cached texture rendering
// Re-render to texture only if dirty
if (dirty || !texture_initialized) {
renderToTexture(cell_width, cell_height);
}
if (!texture_initialized) {
// Fallback to direct rendering if texture creation failed
sf::RectangleShape rect;
rect.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
rect.setOutlineThickness(0);
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); x < x_limit; ++x) {
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); y < y_limit; ++y) {
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
const sf::Color& color = at(x, y);
if (color.a == 0) continue;
auto pixel_pos = sf::Vector2f(
(x * cell_width - left_spritepixels) * zoom,
(y * cell_height - top_spritepixels) * zoom
);
rect.setPosition(pixel_pos);
rect.setFillColor(color);
target.draw(rect);
}
}
return;
}
// Blit visible portion of cached texture with zoom applied
// Calculate source rectangle (unzoomed pixel coordinates in cached texture)
int src_left = std::max(0, (int)left_spritepixels);
int src_top = std::max(0, (int)top_spritepixels);
int src_width = std::min((int)cached_texture.getSize().x - src_left,
(int)((x_limit - left_edge + 2) * cell_width));
int src_height = std::min((int)cached_texture.getSize().y - src_top,
(int)((y_limit - top_edge + 2) * cell_height));
if (src_width <= 0 || src_height <= 0) return;
// Set texture rect for visible portion
cached_sprite.setTextureRect(sf::IntRect({src_left, src_top}, {src_width, src_height}));
// Position in target (offset for partial cell visibility)
float dest_x = (src_left - left_spritepixels) * zoom;
float dest_y = (src_top - top_spritepixels) * zoom;
cached_sprite.setPosition(sf::Vector2f(dest_x, dest_y));
// Apply zoom via scale
cached_sprite.setScale(sf::Vector2f(zoom, zoom));
target.draw(cached_sprite);
}
// =============================================================================
// TileLayer implementation
// =============================================================================
TileLayer::TileLayer(int z_index, int grid_x, int grid_y, UIGrid* parent,
std::shared_ptr<PyTexture> texture)
: GridLayer(GridLayerType::Tile, z_index, grid_x, grid_y, parent),
tiles(grid_x * grid_y, -1), // -1 = no tile
texture(texture)
{}
int& TileLayer::at(int x, int y) {
return tiles[y * grid_x + x];
}
int TileLayer::at(int x, int y) const {
return tiles[y * grid_x + x];
}
void TileLayer::fill(int tile_index) {
std::fill(tiles.begin(), tiles.end(), tile_index);
markDirty(); // #148 - Mark for re-render
}
void TileLayer::resize(int new_grid_x, int new_grid_y) {
std::vector<int> new_tiles(new_grid_x * new_grid_y, -1);
// Copy existing data
int copy_x = std::min(grid_x, new_grid_x);
int copy_y = std::min(grid_y, new_grid_y);
for (int y = 0; y < copy_y; ++y) {
for (int x = 0; x < copy_x; ++x) {
new_tiles[y * new_grid_x + x] = tiles[y * grid_x + x];
}
}
tiles = std::move(new_tiles);
grid_x = new_grid_x;
grid_y = new_grid_y;
// #148 - Invalidate cached texture (will be resized on next render)
texture_initialized = false;
markDirty();
}
// #148 - Render all cells to cached texture (called when dirty)
void TileLayer::renderToTexture(int cell_width, int cell_height) {
ensureTextureSize(cell_width, cell_height);
if (!texture_initialized || !texture) return;
cached_texture.clear(sf::Color::Transparent);
// Render all tiles to cached texture (no zoom - 1:1 pixel mapping)
for (int x = 0; x < grid_x; ++x) {
for (int y = 0; y < grid_y; ++y) {
int tile_index = at(x, y);
if (tile_index < 0) continue; // No tile
auto pixel_pos = sf::Vector2f(x * cell_width, y * cell_height);
sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(1.0f, 1.0f));
cached_texture.draw(sprite);
}
}
cached_texture.display();
dirty = false;
}
void TileLayer::render(sf::RenderTarget& target,
float left_spritepixels, float top_spritepixels,
int left_edge, int top_edge, int x_limit, int y_limit,
float zoom, int cell_width, int cell_height) {
if (!visible || !texture) return;
// #148 - Use cached texture rendering
// Re-render to texture only if dirty
if (dirty || !texture_initialized) {
renderToTexture(cell_width, cell_height);
}
if (!texture_initialized) {
// Fallback to direct rendering if texture creation failed
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); x < x_limit; ++x) {
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); y < y_limit; ++y) {
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
int tile_index = at(x, y);
if (tile_index < 0) continue;
auto pixel_pos = sf::Vector2f(
(x * cell_width - left_spritepixels) * zoom,
(y * cell_height - top_spritepixels) * zoom
);
sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(zoom, zoom));
target.draw(sprite);
}
}
return;
}
// Blit visible portion of cached texture with zoom applied
// Calculate source rectangle (unzoomed pixel coordinates in cached texture)
int src_left = std::max(0, (int)left_spritepixels);
int src_top = std::max(0, (int)top_spritepixels);
int src_width = std::min((int)cached_texture.getSize().x - src_left,
(int)((x_limit - left_edge + 2) * cell_width));
int src_height = std::min((int)cached_texture.getSize().y - src_top,
(int)((y_limit - top_edge + 2) * cell_height));
if (src_width <= 0 || src_height <= 0) return;
// Set texture rect for visible portion
cached_sprite.setTextureRect(sf::IntRect({src_left, src_top}, {src_width, src_height}));
// Position in target (offset for partial cell visibility)
float dest_x = (src_left - left_spritepixels) * zoom;
float dest_y = (src_top - top_spritepixels) * zoom;
cached_sprite.setPosition(sf::Vector2f(dest_x, dest_y));
// Apply zoom via scale
cached_sprite.setScale(sf::Vector2f(zoom, zoom));
target.draw(cached_sprite);
}
// =============================================================================
// Python API - ColorLayer
// =============================================================================
PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = {
{"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS,
"at(x, y) -> Color\n\nGet the color at cell position (x, y)."},
{"set", (PyCFunction)PyGridLayerAPI::ColorLayer_set, METH_VARARGS,
"set(x, y, color)\n\nSet the color at cell position (x, y)."},
{"fill", (PyCFunction)PyGridLayerAPI::ColorLayer_fill, METH_VARARGS,
"fill(color)\n\nFill the entire layer with the specified color."},
{NULL}
};
PyGetSetDef PyGridLayerAPI::ColorLayer_getsetters[] = {
{"z_index", (getter)PyGridLayerAPI::ColorLayer_get_z_index,
(setter)PyGridLayerAPI::ColorLayer_set_z_index,
"Layer z-order. Negative values render below entities.", NULL},
{"visible", (getter)PyGridLayerAPI::ColorLayer_get_visible,
(setter)PyGridLayerAPI::ColorLayer_set_visible,
"Whether the layer is rendered.", NULL},
{"grid_size", (getter)PyGridLayerAPI::ColorLayer_get_grid_size, NULL,
"Layer dimensions as (width, height) tuple.", NULL},
{NULL}
};
int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"z_index", "grid_size", NULL};
int z_index = -1;
PyObject* grid_size_obj = nullptr;
int grid_x = 0, grid_y = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iO", const_cast<char**>(kwlist),
&z_index, &grid_size_obj)) {
return -1;
}
// Parse grid_size if provided
if (grid_size_obj && grid_size_obj != Py_None) {
if (!PyTuple_Check(grid_size_obj) || PyTuple_Size(grid_size_obj) != 2) {
PyErr_SetString(PyExc_TypeError, "grid_size must be a (width, height) tuple");
return -1;
}
grid_x = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 0));
grid_y = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 1));
if (PyErr_Occurred()) return -1;
}
// Create the layer (will be attached to grid via add_layer)
self->data = std::make_shared<ColorLayer>(z_index, grid_x, grid_y, nullptr);
self->grid.reset();
return 0;
}
PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args) {
int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
return NULL;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
return NULL;
}
const sf::Color& color = self->data->at(x, y);
// Return as mcrfpy.Color
auto* color_type = (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Color");
if (!color_type) return NULL;
PyColorObject* color_obj = (PyColorObject*)color_type->tp_alloc(color_type, 0);
Py_DECREF(color_type);
if (!color_obj) return NULL;
color_obj->data = color;
return (PyObject*)color_obj;
}
PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* args) {
int x, y;
PyObject* color_obj;
if (!PyArg_ParseTuple(args, "iiO", &x, &y, &color_obj)) {
return NULL;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
return NULL;
}
// Parse color
sf::Color color;
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
if (!mcrfpy_module) return NULL;
auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color");
Py_DECREF(mcrfpy_module);
if (!color_type) return NULL;
if (PyObject_IsInstance(color_obj, color_type)) {
color = ((PyColorObject*)color_obj)->data;
} else if (PyTuple_Check(color_obj)) {
int r, g, b, a = 255;
if (!PyArg_ParseTuple(color_obj, "iii|i", &r, &g, &b, &a)) {
Py_DECREF(color_type);
return NULL;
}
color = sf::Color(r, g, b, a);
} else {
Py_DECREF(color_type);
PyErr_SetString(PyExc_TypeError, "color must be a Color object or (r, g, b[, a]) tuple");
return NULL;
}
Py_DECREF(color_type);
self->data->at(x, y) = color;
self->data->markDirty(); // #148 - Mark for re-render
Py_RETURN_NONE;
}
PyObject* PyGridLayerAPI::ColorLayer_fill(PyColorLayerObject* self, PyObject* args) {
PyObject* color_obj;
if (!PyArg_ParseTuple(args, "O", &color_obj)) {
return NULL;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
// Parse color
sf::Color color;
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
if (!mcrfpy_module) return NULL;
auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color");
Py_DECREF(mcrfpy_module);
if (!color_type) return NULL;
if (PyObject_IsInstance(color_obj, color_type)) {
color = ((PyColorObject*)color_obj)->data;
} else if (PyTuple_Check(color_obj)) {
int r, g, b, a = 255;
if (!PyArg_ParseTuple(color_obj, "iii|i", &r, &g, &b, &a)) {
Py_DECREF(color_type);
return NULL;
}
color = sf::Color(r, g, b, a);
} else {
Py_DECREF(color_type);
PyErr_SetString(PyExc_TypeError, "color must be a Color object or (r, g, b[, a]) tuple");
return NULL;
}
Py_DECREF(color_type);
self->data->fill(color);
Py_RETURN_NONE;
}
PyObject* PyGridLayerAPI::ColorLayer_get_z_index(PyColorLayerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
return PyLong_FromLong(self->data->z_index);
}
int PyGridLayerAPI::ColorLayer_set_z_index(PyColorLayerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return -1;
}
long z = PyLong_AsLong(value);
if (PyErr_Occurred()) return -1;
self->data->z_index = z;
// TODO: Trigger re-sort in parent grid
return 0;
}
PyObject* PyGridLayerAPI::ColorLayer_get_visible(PyColorLayerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
return PyBool_FromLong(self->data->visible);
}
int PyGridLayerAPI::ColorLayer_set_visible(PyColorLayerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return -1;
}
int v = PyObject_IsTrue(value);
if (v < 0) return -1;
self->data->visible = v;
return 0;
}
PyObject* PyGridLayerAPI::ColorLayer_get_grid_size(PyColorLayerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
}
PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) {
std::ostringstream ss;
if (!self->data) {
ss << "<ColorLayer (invalid)>";
} else {
ss << "<ColorLayer z_index=" << self->data->z_index
<< " size=(" << self->data->grid_x << "x" << self->data->grid_y << ")"
<< " visible=" << (self->data->visible ? "True" : "False") << ">";
}
return PyUnicode_FromString(ss.str().c_str());
}
// =============================================================================
// Python API - TileLayer
// =============================================================================
PyMethodDef PyGridLayerAPI::TileLayer_methods[] = {
{"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS,
"at(x, y) -> int\n\nGet the tile index at cell position (x, y). Returns -1 if no tile."},
{"set", (PyCFunction)PyGridLayerAPI::TileLayer_set, METH_VARARGS,
"set(x, y, index)\n\nSet the tile index at cell position (x, y). Use -1 for no tile."},
{"fill", (PyCFunction)PyGridLayerAPI::TileLayer_fill, METH_VARARGS,
"fill(index)\n\nFill the entire layer with the specified tile index."},
{NULL}
};
PyGetSetDef PyGridLayerAPI::TileLayer_getsetters[] = {
{"z_index", (getter)PyGridLayerAPI::TileLayer_get_z_index,
(setter)PyGridLayerAPI::TileLayer_set_z_index,
"Layer z-order. Negative values render below entities.", NULL},
{"visible", (getter)PyGridLayerAPI::TileLayer_get_visible,
(setter)PyGridLayerAPI::TileLayer_set_visible,
"Whether the layer is rendered.", NULL},
{"texture", (getter)PyGridLayerAPI::TileLayer_get_texture,
(setter)PyGridLayerAPI::TileLayer_set_texture,
"Texture atlas for tile sprites.", NULL},
{"grid_size", (getter)PyGridLayerAPI::TileLayer_get_grid_size, NULL,
"Layer dimensions as (width, height) tuple.", NULL},
{NULL}
};
int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"z_index", "texture", "grid_size", NULL};
int z_index = -1;
PyObject* texture_obj = nullptr;
PyObject* grid_size_obj = nullptr;
int grid_x = 0, grid_y = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iOO", const_cast<char**>(kwlist),
&z_index, &texture_obj, &grid_size_obj)) {
return -1;
}
// Parse texture
std::shared_ptr<PyTexture> texture;
if (texture_obj && texture_obj != Py_None) {
// Check if it's a PyTexture
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
if (!mcrfpy_module) return -1;
auto* texture_type = PyObject_GetAttrString(mcrfpy_module, "Texture");
Py_DECREF(mcrfpy_module);
if (!texture_type) return -1;
if (PyObject_IsInstance(texture_obj, texture_type)) {
texture = ((PyTextureObject*)texture_obj)->data;
} else {
Py_DECREF(texture_type);
PyErr_SetString(PyExc_TypeError, "texture must be a Texture object");
return -1;
}
Py_DECREF(texture_type);
}
// Parse grid_size if provided
if (grid_size_obj && grid_size_obj != Py_None) {
if (!PyTuple_Check(grid_size_obj) || PyTuple_Size(grid_size_obj) != 2) {
PyErr_SetString(PyExc_TypeError, "grid_size must be a (width, height) tuple");
return -1;
}
grid_x = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 0));
grid_y = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 1));
if (PyErr_Occurred()) return -1;
}
// Create the layer
self->data = std::make_shared<TileLayer>(z_index, grid_x, grid_y, nullptr, texture);
self->grid.reset();
return 0;
}
PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args) {
int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
return NULL;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
return NULL;
}
return PyLong_FromLong(self->data->at(x, y));
}
PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args) {
int x, y, index;
if (!PyArg_ParseTuple(args, "iii", &x, &y, &index)) {
return NULL;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
return NULL;
}
self->data->at(x, y) = index;
self->data->markDirty(); // #148 - Mark for re-render
Py_RETURN_NONE;
}
PyObject* PyGridLayerAPI::TileLayer_fill(PyTileLayerObject* self, PyObject* args) {
int index;
if (!PyArg_ParseTuple(args, "i", &index)) {
return NULL;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
self->data->fill(index);
Py_RETURN_NONE;
}
PyObject* PyGridLayerAPI::TileLayer_get_z_index(PyTileLayerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
return PyLong_FromLong(self->data->z_index);
}
int PyGridLayerAPI::TileLayer_set_z_index(PyTileLayerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return -1;
}
long z = PyLong_AsLong(value);
if (PyErr_Occurred()) return -1;
self->data->z_index = z;
// TODO: Trigger re-sort in parent grid
return 0;
}
PyObject* PyGridLayerAPI::TileLayer_get_visible(PyTileLayerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
return PyBool_FromLong(self->data->visible);
}
int PyGridLayerAPI::TileLayer_set_visible(PyTileLayerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return -1;
}
int v = PyObject_IsTrue(value);
if (v < 0) return -1;
self->data->visible = v;
return 0;
}
PyObject* PyGridLayerAPI::TileLayer_get_texture(PyTileLayerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
if (!self->data->texture) {
Py_RETURN_NONE;
}
auto* texture_type = (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Texture");
if (!texture_type) return NULL;
PyTextureObject* tex_obj = (PyTextureObject*)texture_type->tp_alloc(texture_type, 0);
Py_DECREF(texture_type);
if (!tex_obj) return NULL;
tex_obj->data = self->data->texture;
return (PyObject*)tex_obj;
}
int PyGridLayerAPI::TileLayer_set_texture(PyTileLayerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return -1;
}
if (value == Py_None) {
self->data->texture.reset();
self->data->markDirty(); // #148 - Mark for re-render
return 0;
}
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
if (!mcrfpy_module) return -1;
auto* texture_type = PyObject_GetAttrString(mcrfpy_module, "Texture");
Py_DECREF(mcrfpy_module);
if (!texture_type) return -1;
if (!PyObject_IsInstance(value, texture_type)) {
Py_DECREF(texture_type);
PyErr_SetString(PyExc_TypeError, "texture must be a Texture object or None");
return -1;
}
Py_DECREF(texture_type);
self->data->texture = ((PyTextureObject*)value)->data;
self->data->markDirty(); // #148 - Mark for re-render
return 0;
}
PyObject* PyGridLayerAPI::TileLayer_get_grid_size(PyTileLayerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
}
PyObject* PyGridLayerAPI::TileLayer_repr(PyTileLayerObject* self) {
std::ostringstream ss;
if (!self->data) {
ss << "<TileLayer (invalid)>";
} else {
ss << "<TileLayer z_index=" << self->data->z_index
<< " size=(" << self->data->grid_x << "x" << self->data->grid_y << ")"
<< " visible=" << (self->data->visible ? "True" : "False")
<< " texture=" << (self->data->texture ? "set" : "None") << ">";
}
return PyUnicode_FromString(ss.str().c_str());
}

View File

@ -1,244 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "structmember.h"
#include <SFML/Graphics.hpp>
#include <memory>
#include <vector>
#include <string>
// Forward declarations
class UIGrid;
class PyTexture;
// Include PyTexture.h for PyTextureObject (typedef, not struct)
#include "PyTexture.h"
// Layer type enumeration
enum class GridLayerType {
Color,
Tile
};
// Abstract base class for grid layers
class GridLayer {
public:
GridLayerType type;
std::string name; // #150 - Layer name for GridPoint property access
int z_index; // Negative = below entities, >= 0 = above entities
int grid_x, grid_y; // Dimensions
UIGrid* parent_grid; // Parent grid reference
bool visible; // Visibility flag
// #148 - Dirty flag and RenderTexture caching
bool dirty; // True if layer needs re-render
sf::RenderTexture cached_texture; // Cached layer content
sf::Sprite cached_sprite; // Sprite for blitting cached texture
bool texture_initialized; // True if RenderTexture has been created
int cached_cell_width, cached_cell_height; // Cell size used for cached texture
GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent);
virtual ~GridLayer() = default;
// Mark layer as needing re-render
void markDirty();
// Ensure cached texture is properly sized for current grid dimensions
void ensureTextureSize(int cell_width, int cell_height);
// Render the layer content to the cached texture (called when dirty)
virtual void renderToTexture(int cell_width, int cell_height) = 0;
// Render the layer to a RenderTarget with the given transformation parameters
// Uses cached texture if available, only re-renders when dirty
virtual void render(sf::RenderTarget& target,
float left_spritepixels, float top_spritepixels,
int left_edge, int top_edge, int x_limit, int y_limit,
float zoom, int cell_width, int cell_height) = 0;
// Resize the layer (reallocates storage)
virtual void resize(int new_grid_x, int new_grid_y) = 0;
};
// Color layer - stores RGBA color per cell
class ColorLayer : public GridLayer {
public:
std::vector<sf::Color> colors;
ColorLayer(int z_index, int grid_x, int grid_y, UIGrid* parent);
// Access color at position
sf::Color& at(int x, int y);
const sf::Color& at(int x, int y) const;
// Fill entire layer with a color
void fill(const sf::Color& color);
// #148 - Render all content to cached texture
void renderToTexture(int cell_width, int cell_height) override;
void render(sf::RenderTarget& target,
float left_spritepixels, float top_spritepixels,
int left_edge, int top_edge, int x_limit, int y_limit,
float zoom, int cell_width, int cell_height) override;
void resize(int new_grid_x, int new_grid_y) override;
};
// Tile layer - stores sprite index per cell with texture reference
class TileLayer : public GridLayer {
public:
std::vector<int> tiles; // Sprite indices (-1 = no tile)
std::shared_ptr<PyTexture> texture;
TileLayer(int z_index, int grid_x, int grid_y, UIGrid* parent,
std::shared_ptr<PyTexture> texture = nullptr);
// Access tile index at position
int& at(int x, int y);
int at(int x, int y) const;
// Fill entire layer with a tile index
void fill(int tile_index);
// #148 - Render all content to cached texture
void renderToTexture(int cell_width, int cell_height) override;
void render(sf::RenderTarget& target,
float left_spritepixels, float top_spritepixels,
int left_edge, int top_edge, int x_limit, int y_limit,
float zoom, int cell_width, int cell_height) override;
void resize(int new_grid_x, int new_grid_y) override;
};
// Python wrapper types
typedef struct {
PyObject_HEAD
std::shared_ptr<GridLayer> data;
std::shared_ptr<UIGrid> grid; // Parent grid reference
} PyGridLayerObject;
typedef struct {
PyObject_HEAD
std::shared_ptr<ColorLayer> data;
std::shared_ptr<UIGrid> grid;
} PyColorLayerObject;
typedef struct {
PyObject_HEAD
std::shared_ptr<TileLayer> data;
std::shared_ptr<UIGrid> grid;
} PyTileLayerObject;
// Python API classes
class PyGridLayerAPI {
public:
// ColorLayer methods
static int ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args);
static PyObject* ColorLayer_set(PyColorLayerObject* self, PyObject* args);
static PyObject* ColorLayer_fill(PyColorLayerObject* self, PyObject* args);
static PyObject* ColorLayer_get_z_index(PyColorLayerObject* self, void* closure);
static int ColorLayer_set_z_index(PyColorLayerObject* self, PyObject* value, void* closure);
static PyObject* ColorLayer_get_visible(PyColorLayerObject* self, void* closure);
static int ColorLayer_set_visible(PyColorLayerObject* self, PyObject* value, void* closure);
static PyObject* ColorLayer_get_grid_size(PyColorLayerObject* self, void* closure);
static PyObject* ColorLayer_repr(PyColorLayerObject* self);
// TileLayer methods
static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args);
static PyObject* TileLayer_set(PyTileLayerObject* self, PyObject* args);
static PyObject* TileLayer_fill(PyTileLayerObject* self, PyObject* args);
static PyObject* TileLayer_get_z_index(PyTileLayerObject* self, void* closure);
static int TileLayer_set_z_index(PyTileLayerObject* self, PyObject* value, void* closure);
static PyObject* TileLayer_get_visible(PyTileLayerObject* self, void* closure);
static int TileLayer_set_visible(PyTileLayerObject* self, PyObject* value, void* closure);
static PyObject* TileLayer_get_texture(PyTileLayerObject* self, void* closure);
static int TileLayer_set_texture(PyTileLayerObject* self, PyObject* value, void* closure);
static PyObject* TileLayer_get_grid_size(PyTileLayerObject* self, void* closure);
static PyObject* TileLayer_repr(PyTileLayerObject* self);
// Method and getset arrays
static PyMethodDef ColorLayer_methods[];
static PyGetSetDef ColorLayer_getsetters[];
static PyMethodDef TileLayer_methods[];
static PyGetSetDef TileLayer_getsetters[];
};
namespace mcrfpydef {
// ColorLayer type
static PyTypeObject PyColorLayerType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.ColorLayer",
.tp_basicsize = sizeof(PyColorLayerObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self) {
PyColorLayerObject* obj = (PyColorLayerObject*)self;
obj->data.reset();
obj->grid.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("ColorLayer(z_index=-1, grid_size=None)\n\n"
"A grid layer that stores RGBA colors per cell.\n\n"
"Args:\n"
" z_index (int): Render order. Negative = below entities. Default: -1\n"
" grid_size (tuple): Dimensions as (width, height). Default: parent grid size\n\n"
"Attributes:\n"
" z_index (int): Layer z-order relative to entities\n"
" visible (bool): Whether layer is rendered\n"
" grid_size (tuple): Layer dimensions (read-only)\n\n"
"Methods:\n"
" at(x, y): Get color at cell position\n"
" set(x, y, color): Set color at cell position\n"
" fill(color): Fill entire layer with color"),
.tp_methods = PyGridLayerAPI::ColorLayer_methods,
.tp_getset = PyGridLayerAPI::ColorLayer_getsetters,
.tp_init = (initproc)PyGridLayerAPI::ColorLayer_init,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
PyColorLayerObject* self = (PyColorLayerObject*)type->tp_alloc(type, 0);
return (PyObject*)self;
}
};
// TileLayer type
static PyTypeObject PyTileLayerType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.TileLayer",
.tp_basicsize = sizeof(PyTileLayerObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self) {
PyTileLayerObject* obj = (PyTileLayerObject*)self;
obj->data.reset();
obj->grid.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("TileLayer(z_index=-1, texture=None, grid_size=None)\n\n"
"A grid layer that stores sprite indices per cell.\n\n"
"Args:\n"
" z_index (int): Render order. Negative = below entities. Default: -1\n"
" texture (Texture): Sprite atlas for tile rendering. Default: None\n"
" grid_size (tuple): Dimensions as (width, height). Default: parent grid size\n\n"
"Attributes:\n"
" z_index (int): Layer z-order relative to entities\n"
" visible (bool): Whether layer is rendered\n"
" texture (Texture): Tile sprite atlas\n"
" grid_size (tuple): Layer dimensions (read-only)\n\n"
"Methods:\n"
" at(x, y): Get tile index at cell position\n"
" set(x, y, index): Set tile index at cell position\n"
" fill(index): Fill entire layer with tile index"),
.tp_methods = PyGridLayerAPI::TileLayer_methods,
.tp_getset = PyGridLayerAPI::TileLayer_getsetters,
.tp_init = (initproc)PyGridLayerAPI::TileLayer_init,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
PyTileLayerObject* self = (PyTileLayerObject*)type->tp_alloc(type, 0);
return (PyObject*)self;
}
};
}

View File

@ -1,27 +0,0 @@
#include "HeadlessRenderer.h"
#include <iostream>
bool HeadlessRenderer::init(int width, int height) {
if (!render_texture.create(width, height)) {
std::cerr << "Failed to create headless render texture" << std::endl;
return false;
}
return true;
}
sf::RenderTarget& HeadlessRenderer::getRenderTarget() {
return render_texture;
}
void HeadlessRenderer::saveScreenshot(const std::string& path) {
sf::Image screenshot = render_texture.getTexture().copyToImage();
if (!screenshot.saveToFile(path)) {
std::cerr << "Failed to save screenshot to: " << path << std::endl;
} else {
std::cout << "Screenshot saved to: " << path << std::endl;
}
}
void HeadlessRenderer::display() {
render_texture.display();
}

View File

@ -1,20 +0,0 @@
#ifndef HEADLESS_RENDERER_H
#define HEADLESS_RENDERER_H
#include <SFML/Graphics.hpp>
#include <memory>
#include <string>
class HeadlessRenderer {
private:
sf::RenderTexture render_texture;
public:
bool init(int width = 1024, int height = 768);
sf::RenderTarget& getRenderTarget();
void saveScreenshot(const std::string& path);
void display(); // Finalize the current frame
bool isOpen() const { return true; } // Always "open" in headless mode
};
#endif // HEADLESS_RENDERER_H

View File

@ -1,246 +0,0 @@
#include "ImGuiConsole.h"
#include "imgui.h"
#include "McRFPy_API.h"
#include <Python.h>
#include <sstream>
// Static member initialization
bool ImGuiConsole::enabled = true;
ImGuiConsole::ImGuiConsole() {
addOutput("McRogueFace Python Console", false);
addOutput("Type Python commands and press Enter to execute.", false);
addOutput("", false);
}
void ImGuiConsole::toggle() {
if (enabled) {
visible = !visible;
if (visible) {
// Focus input when opening
ImGui::SetWindowFocus("Console");
}
}
}
bool ImGuiConsole::wantsKeyboardInput() const {
return visible && enabled;
}
void ImGuiConsole::addOutput(const std::string& text, bool isError) {
// Split text by newlines and add each line separately
std::istringstream stream(text);
std::string line;
while (std::getline(stream, line)) {
outputHistory.push_back({line, isError, false});
}
// Trim history if too long
while (outputHistory.size() > MAX_HISTORY) {
outputHistory.pop_front();
}
scrollToBottom = true;
}
void ImGuiConsole::executeCommand(const std::string& command) {
if (command.empty()) return;
// Add command to output with >>> prefix
outputHistory.push_back({">>> " + command, false, true});
// Add to command history
commandHistory.push_back(command);
historyIndex = -1;
// Capture Python output
// Redirect stdout/stderr to capture output
std::string captureCode = R"(
import sys
import io
_console_stdout = io.StringIO()
_console_stderr = io.StringIO()
_old_stdout = sys.stdout
_old_stderr = sys.stderr
sys.stdout = _console_stdout
sys.stderr = _console_stderr
)";
std::string restoreCode = R"(
sys.stdout = _old_stdout
sys.stderr = _old_stderr
_stdout_val = _console_stdout.getvalue()
_stderr_val = _console_stderr.getvalue()
)";
// Set up capture
PyRun_SimpleString(captureCode.c_str());
// Try to evaluate as expression first (for things like "2+2")
PyObject* main_module = PyImport_AddModule("__main__");
PyObject* main_dict = PyModule_GetDict(main_module);
// First try eval (for expressions that return values)
PyObject* result = PyRun_String(command.c_str(), Py_eval_input, main_dict, main_dict);
bool showedResult = false;
if (result == nullptr) {
// Clear the error from eval attempt
PyErr_Clear();
// Try exec (for statements)
result = PyRun_String(command.c_str(), Py_file_input, main_dict, main_dict);
if (result == nullptr) {
// Real error - capture it
PyErr_Print(); // This prints to stderr which we're capturing
}
} else if (result != Py_None) {
// Expression returned a non-None value - show its repr
PyObject* repr = PyObject_Repr(result);
if (repr) {
const char* repr_str = PyUnicode_AsUTF8(repr);
if (repr_str) {
addOutput(repr_str, false);
showedResult = true;
}
Py_DECREF(repr);
}
}
Py_XDECREF(result);
// Restore stdout/stderr
PyRun_SimpleString(restoreCode.c_str());
// Get captured stdout (only if we didn't already show a result)
PyObject* stdout_val = PyObject_GetAttrString(main_module, "_stdout_val");
if (stdout_val && PyUnicode_Check(stdout_val)) {
const char* stdout_str = PyUnicode_AsUTF8(stdout_val);
if (stdout_str && strlen(stdout_str) > 0) {
addOutput(stdout_str, false);
}
}
Py_XDECREF(stdout_val);
// Get captured stderr
PyObject* stderr_val = PyObject_GetAttrString(main_module, "_stderr_val");
if (stderr_val && PyUnicode_Check(stderr_val)) {
const char* stderr_str = PyUnicode_AsUTF8(stderr_val);
if (stderr_str && strlen(stderr_str) > 0) {
addOutput(stderr_str, true);
}
}
Py_XDECREF(stderr_val);
// Clean up temporary variables
PyRun_SimpleString("del _console_stdout, _console_stderr, _old_stdout, _old_stderr, _stdout_val, _stderr_val");
scrollToBottom = true;
}
void ImGuiConsole::render() {
if (!visible || !enabled) return;
// Set up console window
ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y * 0.4f), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_FirstUseEver);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
if (!ImGui::Begin("Console", &visible, flags)) {
ImGui::End();
return;
}
// Output area (scrollable, no horizontal scrollbar - use word wrap)
float footerHeight = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing();
ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footerHeight), false, ImGuiWindowFlags_None);
// Render output lines with word wrap
for (const auto& line : outputHistory) {
if (line.isInput) {
// User input - yellow/gold color
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.9f, 0.4f, 1.0f));
} else if (line.isError) {
// Error - red color
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
} else {
// Normal output - default color
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.8f, 0.8f, 1.0f));
}
ImGui::TextWrapped("%s", line.text.c_str());
ImGui::PopStyleColor();
}
// Auto-scroll to bottom when new content is added
if (scrollToBottom || ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
ImGui::SetScrollHereY(1.0f);
}
scrollToBottom = false;
ImGui::EndChild();
// Input line
ImGui::Separator();
// Input field
ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue |
ImGuiInputTextFlags_CallbackHistory |
ImGuiInputTextFlags_CallbackCompletion;
bool reclaimFocus = false;
// Custom callback for history navigation
auto callback = [](ImGuiInputTextCallbackData* data) -> int {
ImGuiConsole* console = static_cast<ImGuiConsole*>(data->UserData);
if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) {
if (console->commandHistory.empty()) return 0;
if (data->EventKey == ImGuiKey_UpArrow) {
if (console->historyIndex < 0) {
console->historyIndex = static_cast<int>(console->commandHistory.size()) - 1;
} else if (console->historyIndex > 0) {
console->historyIndex--;
}
} else if (data->EventKey == ImGuiKey_DownArrow) {
if (console->historyIndex >= 0) {
console->historyIndex++;
if (console->historyIndex >= static_cast<int>(console->commandHistory.size())) {
console->historyIndex = -1;
}
}
}
// Update input buffer
if (console->historyIndex >= 0 && console->historyIndex < static_cast<int>(console->commandHistory.size())) {
const std::string& historyEntry = console->commandHistory[console->historyIndex];
data->DeleteChars(0, data->BufTextLen);
data->InsertChars(0, historyEntry.c_str());
} else {
data->DeleteChars(0, data->BufTextLen);
}
}
return 0;
};
ImGui::PushItemWidth(-1); // Full width
if (ImGui::InputText("##Input", inputBuffer, sizeof(inputBuffer), inputFlags, callback, this)) {
std::string command(inputBuffer);
inputBuffer[0] = '\0';
executeCommand(command);
reclaimFocus = true;
}
ImGui::PopItemWidth();
// Keep focus on input
ImGui::SetItemDefaultFocus();
if (reclaimFocus || (visible && !ImGui::IsAnyItemActive())) {
ImGui::SetKeyboardFocusHere(-1);
}
ImGui::End();
}

View File

@ -1,56 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include <deque>
/**
* @brief ImGui-based debug console for Python REPL
*
* Provides an overlay console that can execute Python code
* without blocking the main game loop. Activated by grave/tilde key.
*/
class ImGuiConsole {
public:
ImGuiConsole();
// Core functionality
void render(); // Render the console UI
void toggle(); // Toggle visibility
bool isVisible() const { return visible; }
void setVisible(bool v) { visible = v; }
// Configuration (for Python API)
static bool isEnabled() { return enabled; }
static void setEnabled(bool e) { enabled = e; }
// Input handling
bool wantsKeyboardInput() const; // Returns true if ImGui wants keyboard
private:
void executeCommand(const std::string& command);
void addOutput(const std::string& text, bool isError = false);
// State
bool visible = false;
static bool enabled; // Global enable/disable (for shipping games)
// Input buffer
char inputBuffer[1024] = {0};
// Output history
struct OutputLine {
std::string text;
bool isError;
bool isInput; // True if this was user input (for styling)
};
std::deque<OutputLine> outputHistory;
static constexpr size_t MAX_HISTORY = 500;
// Command history for up/down navigation
std::vector<std::string> commandHistory;
int historyIndex = -1;
// Scroll state
bool scrollToBottom = true;
};

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,9 @@
#include "Common.h" #include "Common.h"
#include "Python.h" #include "Python.h"
#include <list> #include <list>
#include <atomic>
#include "PyFont.h" #include "PyFont.h"
#include "PyTexture.h" #include "PyTexture.h"
#include "McRogueFaceConfig.h"
class GameEngine; // forward declared (circular members) class GameEngine; // forward declared (circular members)
@ -29,18 +27,19 @@ public:
//static void setSpriteTexture(int); //static void setSpriteTexture(int);
inline static GameEngine* game; inline static GameEngine* game;
static void api_init(); static void api_init();
static void api_init(const McRogueFaceConfig& config);
static PyStatus init_python_with_config(const McRogueFaceConfig& config);
static void api_shutdown(); static void api_shutdown();
// Python API functionality - use mcrfpy.* in scripts // Python API functionality - use mcrfpy.* in scripts
//static PyObject* _drawSprite(PyObject*, PyObject*); //static PyObject* _drawSprite(PyObject*, PyObject*);
static void REPL_device(FILE * fp, const char *filename); static void REPL_device(FILE * fp, const char *filename);
static void REPL(); static void REPL();
static std::vector<sf::SoundBuffer>* soundbuffers; static std::vector<sf::SoundBuffer> soundbuffers;
static sf::Music* music; static sf::Music music;
static sf::Sound* sfx; static sf::Sound sfx;
static std::map<std::string, PyObject*> callbacks;
static PyObject* _registerPyAction(PyObject*, PyObject*);
static PyObject* _registerInputAction(PyObject*, PyObject*);
static PyObject* _createSoundBuffer(PyObject*, PyObject*); static PyObject* _createSoundBuffer(PyObject*, PyObject*);
static PyObject* _loadMusic(PyObject*, PyObject*); static PyObject* _loadMusic(PyObject*, PyObject*);
@ -67,37 +66,12 @@ public:
// accept keyboard input from scene // accept keyboard input from scene
static sf::Vector2i cursor_position; static sf::Vector2i cursor_position;
static void player_input(int, int);
static void computerTurn();
static void playerTurn();
static void doAction(std::string);
static void executeScript(std::string); static void executeScript(std::string);
static void executePyString(std::string); static void executePyString(std::string);
// Helper to mark scenes as needing z_index resort
static void markSceneNeedsSort();
// Name-based finding methods
static PyObject* _find(PyObject*, PyObject*);
static PyObject* _findAll(PyObject*, PyObject*);
// Profiling/metrics
static PyObject* _getMetrics(PyObject*, PyObject*);
// Benchmark logging (#104)
static PyObject* _startBenchmark(PyObject*, PyObject*);
static PyObject* _endBenchmark(PyObject*, PyObject*);
static PyObject* _logBenchmark(PyObject*, PyObject*);
// Developer console
static PyObject* _setDevConsole(PyObject*, PyObject*);
// Scene lifecycle management for Python Scene objects
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
static void updatePythonScenes(float dt);
static void triggerResize(int width, int height);
// Exception handling - signal game loop to exit on unhandled Python exceptions
static std::atomic<bool> exception_occurred;
static std::atomic<int> exit_code;
static void signalPythonException(); // Called by exception handlers
static bool shouldExit(); // Checked by game loop
}; };

View File

@ -1,836 +0,0 @@
#include "McRFPy_Automation.h"
#include "McRFPy_API.h"
#include "GameEngine.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <unordered_map>
// #111 - Static member for simulated mouse position in headless mode
sf::Vector2i McRFPy_Automation::simulated_mouse_pos(0, 0);
// #111 - Get simulated mouse position for headless mode
sf::Vector2i McRFPy_Automation::getSimulatedMousePosition() {
return simulated_mouse_pos;
}
// Helper function to get game engine
GameEngine* McRFPy_Automation::getGameEngine() {
return McRFPy_API::game;
}
// Sleep helper
void McRFPy_Automation::sleep_ms(int milliseconds) {
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
}
// Convert string to SFML key code
sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) {
static const std::unordered_map<std::string, sf::Keyboard::Key> keyMap = {
// Letters
{"a", sf::Keyboard::A}, {"b", sf::Keyboard::B}, {"c", sf::Keyboard::C},
{"d", sf::Keyboard::D}, {"e", sf::Keyboard::E}, {"f", sf::Keyboard::F},
{"g", sf::Keyboard::G}, {"h", sf::Keyboard::H}, {"i", sf::Keyboard::I},
{"j", sf::Keyboard::J}, {"k", sf::Keyboard::K}, {"l", sf::Keyboard::L},
{"m", sf::Keyboard::M}, {"n", sf::Keyboard::N}, {"o", sf::Keyboard::O},
{"p", sf::Keyboard::P}, {"q", sf::Keyboard::Q}, {"r", sf::Keyboard::R},
{"s", sf::Keyboard::S}, {"t", sf::Keyboard::T}, {"u", sf::Keyboard::U},
{"v", sf::Keyboard::V}, {"w", sf::Keyboard::W}, {"x", sf::Keyboard::X},
{"y", sf::Keyboard::Y}, {"z", sf::Keyboard::Z},
// Numbers
{"0", sf::Keyboard::Num0}, {"1", sf::Keyboard::Num1}, {"2", sf::Keyboard::Num2},
{"3", sf::Keyboard::Num3}, {"4", sf::Keyboard::Num4}, {"5", sf::Keyboard::Num5},
{"6", sf::Keyboard::Num6}, {"7", sf::Keyboard::Num7}, {"8", sf::Keyboard::Num8},
{"9", sf::Keyboard::Num9},
// Function keys
{"f1", sf::Keyboard::F1}, {"f2", sf::Keyboard::F2}, {"f3", sf::Keyboard::F3},
{"f4", sf::Keyboard::F4}, {"f5", sf::Keyboard::F5}, {"f6", sf::Keyboard::F6},
{"f7", sf::Keyboard::F7}, {"f8", sf::Keyboard::F8}, {"f9", sf::Keyboard::F9},
{"f10", sf::Keyboard::F10}, {"f11", sf::Keyboard::F11}, {"f12", sf::Keyboard::F12},
{"f13", sf::Keyboard::F13}, {"f14", sf::Keyboard::F14}, {"f15", sf::Keyboard::F15},
// Special keys
{"escape", sf::Keyboard::Escape}, {"esc", sf::Keyboard::Escape},
{"enter", sf::Keyboard::Enter}, {"return", sf::Keyboard::Enter},
{"space", sf::Keyboard::Space}, {" ", sf::Keyboard::Space},
{"tab", sf::Keyboard::Tab}, {"\t", sf::Keyboard::Tab},
{"backspace", sf::Keyboard::BackSpace},
{"delete", sf::Keyboard::Delete}, {"del", sf::Keyboard::Delete},
{"insert", sf::Keyboard::Insert},
{"home", sf::Keyboard::Home},
{"end", sf::Keyboard::End},
{"pageup", sf::Keyboard::PageUp}, {"pgup", sf::Keyboard::PageUp},
{"pagedown", sf::Keyboard::PageDown}, {"pgdn", sf::Keyboard::PageDown},
// Arrow keys
{"left", sf::Keyboard::Left},
{"right", sf::Keyboard::Right},
{"up", sf::Keyboard::Up},
{"down", sf::Keyboard::Down},
// Modifiers
{"ctrl", sf::Keyboard::LControl}, {"ctrlleft", sf::Keyboard::LControl},
{"ctrlright", sf::Keyboard::RControl},
{"alt", sf::Keyboard::LAlt}, {"altleft", sf::Keyboard::LAlt},
{"altright", sf::Keyboard::RAlt},
{"shift", sf::Keyboard::LShift}, {"shiftleft", sf::Keyboard::LShift},
{"shiftright", sf::Keyboard::RShift},
{"win", sf::Keyboard::LSystem}, {"winleft", sf::Keyboard::LSystem},
{"winright", sf::Keyboard::RSystem}, {"command", sf::Keyboard::LSystem},
// Punctuation
{",", sf::Keyboard::Comma}, {".", sf::Keyboard::Period},
{"/", sf::Keyboard::Slash}, {"\\", sf::Keyboard::BackSlash},
{";", sf::Keyboard::SemiColon}, {"'", sf::Keyboard::Quote},
{"[", sf::Keyboard::LBracket}, {"]", sf::Keyboard::RBracket},
{"-", sf::Keyboard::Dash}, {"=", sf::Keyboard::Equal},
// Numpad
{"num0", sf::Keyboard::Numpad0}, {"num1", sf::Keyboard::Numpad1},
{"num2", sf::Keyboard::Numpad2}, {"num3", sf::Keyboard::Numpad3},
{"num4", sf::Keyboard::Numpad4}, {"num5", sf::Keyboard::Numpad5},
{"num6", sf::Keyboard::Numpad6}, {"num7", sf::Keyboard::Numpad7},
{"num8", sf::Keyboard::Numpad8}, {"num9", sf::Keyboard::Numpad9},
{"add", sf::Keyboard::Add}, {"subtract", sf::Keyboard::Subtract},
{"multiply", sf::Keyboard::Multiply}, {"divide", sf::Keyboard::Divide},
// Other
{"pause", sf::Keyboard::Pause},
{"capslock", sf::Keyboard::LControl}, // Note: SFML doesn't have CapsLock
{"numlock", sf::Keyboard::LControl}, // Note: SFML doesn't have NumLock
{"scrolllock", sf::Keyboard::LControl}, // Note: SFML doesn't have ScrollLock
};
auto it = keyMap.find(keyName);
if (it != keyMap.end()) {
return it->second;
}
return sf::Keyboard::Unknown;
}
// Inject mouse event into the game engine
void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button) {
auto engine = getGameEngine();
if (!engine) return;
// #111 - Track simulated mouse position for headless mode
if (type == sf::Event::MouseMoved ||
type == sf::Event::MouseButtonPressed ||
type == sf::Event::MouseButtonReleased) {
simulated_mouse_pos = sf::Vector2i(x, y);
}
sf::Event event;
event.type = type;
switch (type) {
case sf::Event::MouseMoved:
event.mouseMove.x = x;
event.mouseMove.y = y;
break;
case sf::Event::MouseButtonPressed:
case sf::Event::MouseButtonReleased:
event.mouseButton.button = button;
event.mouseButton.x = x;
event.mouseButton.y = y;
break;
case sf::Event::MouseWheelScrolled:
event.mouseWheelScroll.wheel = sf::Mouse::VerticalWheel;
event.mouseWheelScroll.delta = static_cast<float>(x); // x is used for scroll amount
event.mouseWheelScroll.x = x;
event.mouseWheelScroll.y = y;
break;
default:
break;
}
engine->processEvent(event);
}
// Inject keyboard event into the game engine
void McRFPy_Automation::injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key) {
auto engine = getGameEngine();
if (!engine) return;
sf::Event event;
event.type = type;
if (type == sf::Event::KeyPressed || type == sf::Event::KeyReleased) {
event.key.code = key;
event.key.alt = sf::Keyboard::isKeyPressed(sf::Keyboard::LAlt) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RAlt);
event.key.control = sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RControl);
event.key.shift = sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RShift);
event.key.system = sf::Keyboard::isKeyPressed(sf::Keyboard::LSystem) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RSystem);
}
engine->processEvent(event);
}
// Inject text event for typing
void McRFPy_Automation::injectTextEvent(sf::Uint32 unicode) {
auto engine = getGameEngine();
if (!engine) return;
sf::Event event;
event.type = sf::Event::TextEntered;
event.text.unicode = unicode;
engine->processEvent(event);
}
// Screenshot implementation
PyObject* McRFPy_Automation::_screenshot(PyObject* self, PyObject* args) {
const char* filename;
if (!PyArg_ParseTuple(args, "s", &filename)) {
return NULL;
}
auto engine = getGameEngine();
if (!engine) {
PyErr_SetString(PyExc_RuntimeError, "Game engine not initialized");
return NULL;
}
// Get the render target
sf::RenderTarget* target = engine->getRenderTargetPtr();
if (!target) {
PyErr_SetString(PyExc_RuntimeError, "No render target available");
return NULL;
}
// For RenderWindow, we can get a screenshot directly
if (auto* window = dynamic_cast<sf::RenderWindow*>(target)) {
sf::Vector2u windowSize = window->getSize();
sf::Texture texture;
texture.create(windowSize.x, windowSize.y);
texture.update(*window);
if (texture.copyToImage().saveToFile(filename)) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
// For RenderTexture (headless mode)
else if (auto* renderTexture = dynamic_cast<sf::RenderTexture*>(target)) {
if (renderTexture->getTexture().copyToImage().saveToFile(filename)) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
PyErr_SetString(PyExc_RuntimeError, "Unknown render target type");
return NULL;
}
// Get current mouse position
PyObject* McRFPy_Automation::_position(PyObject* self, PyObject* args) {
auto engine = getGameEngine();
if (!engine || !engine->getRenderTargetPtr()) {
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
}
// In headless mode, return the simulated mouse position (#111)
if (engine->isHeadless()) {
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
}
// In windowed mode, return the actual mouse position relative to window
if (auto* window = dynamic_cast<sf::RenderWindow*>(engine->getRenderTargetPtr())) {
sf::Vector2i pos = sf::Mouse::getPosition(*window);
return Py_BuildValue("(ii)", pos.x, pos.y);
}
// Fallback to simulated position
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
}
// Get screen size
PyObject* McRFPy_Automation::_size(PyObject* self, PyObject* args) {
auto engine = getGameEngine();
if (!engine || !engine->getRenderTargetPtr()) {
return Py_BuildValue("(ii)", 1024, 768); // Default size
}
sf::Vector2u size = engine->getRenderTarget().getSize();
return Py_BuildValue("(ii)", size.x, size.y);
}
// Check if coordinates are on screen
PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args) {
int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
return NULL;
}
auto engine = getGameEngine();
if (!engine || !engine->getRenderTargetPtr()) {
Py_RETURN_FALSE;
}
sf::Vector2u size = engine->getRenderTarget().getSize();
if (x >= 0 && x < (int)size.x && y >= 0 && y < (int)size.y) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
// Move mouse to position
PyObject* McRFPy_Automation::_moveTo(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "duration", NULL};
int x, y;
float duration = 0.0f;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
&x, &y, &duration)) {
return NULL;
}
// TODO: Implement smooth movement with duration
injectMouseEvent(sf::Event::MouseMoved, x, y);
if (duration > 0) {
sleep_ms(static_cast<int>(duration * 1000));
}
Py_RETURN_NONE;
}
// Move mouse relative
PyObject* McRFPy_Automation::_moveRel(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"xOffset", "yOffset", "duration", NULL};
int xOffset, yOffset;
float duration = 0.0f;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
&xOffset, &yOffset, &duration)) {
return NULL;
}
// Get current position
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
int currentX, currentY;
if (!PyArg_ParseTuple(pos, "ii", &currentX, &currentY)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
// Move to new position
injectMouseEvent(sf::Event::MouseMoved, currentX + xOffset, currentY + yOffset);
if (duration > 0) {
sleep_ms(static_cast<int>(duration * 1000));
}
Py_RETURN_NONE;
}
// Click implementation
PyObject* McRFPy_Automation::_click(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "clicks", "interval", "button", NULL};
int x = -1, y = -1;
int clicks = 1;
float interval = 0.0f;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iiifs", const_cast<char**>(kwlist),
&x, &y, &clicks, &interval, &button)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
// Determine button
sf::Mouse::Button sfButton = sf::Mouse::Left;
if (strcmp(button, "right") == 0) {
sfButton = sf::Mouse::Right;
} else if (strcmp(button, "middle") == 0) {
sfButton = sf::Mouse::Middle;
}
// Move to position first
injectMouseEvent(sf::Event::MouseMoved, x, y);
// Perform clicks
for (int i = 0; i < clicks; i++) {
if (i > 0 && interval > 0) {
sleep_ms(static_cast<int>(interval * 1000));
}
injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton);
sleep_ms(10); // Small delay between press and release
injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton);
}
Py_RETURN_NONE;
}
// Right click
PyObject* McRFPy_Automation::_rightClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
// Build new args with button="right"
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("right"));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
// Double click
PyObject* McRFPy_Automation::_doubleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(2));
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
// Type text
PyObject* McRFPy_Automation::_typewrite(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"message", "interval", NULL};
const char* message;
float interval = 0.0f;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|f", const_cast<char**>(kwlist),
&message, &interval)) {
return NULL;
}
// Type each character
for (size_t i = 0; message[i] != '\0'; i++) {
if (i > 0 && interval > 0) {
sleep_ms(static_cast<int>(interval * 1000));
}
char c = message[i];
// Handle special characters
if (c == '\n') {
injectKeyEvent(sf::Event::KeyPressed, sf::Keyboard::Enter);
injectKeyEvent(sf::Event::KeyReleased, sf::Keyboard::Enter);
} else if (c == '\t') {
injectKeyEvent(sf::Event::KeyPressed, sf::Keyboard::Tab);
injectKeyEvent(sf::Event::KeyReleased, sf::Keyboard::Tab);
} else {
// For regular characters, send text event
injectTextEvent(static_cast<sf::Uint32>(c));
}
}
Py_RETURN_NONE;
}
// Press and hold key
PyObject* McRFPy_Automation::_keyDown(PyObject* self, PyObject* args) {
const char* keyName;
if (!PyArg_ParseTuple(args, "s", &keyName)) {
return NULL;
}
sf::Keyboard::Key key = stringToKey(keyName);
if (key == sf::Keyboard::Unknown) {
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
return NULL;
}
injectKeyEvent(sf::Event::KeyPressed, key);
Py_RETURN_NONE;
}
// Release key
PyObject* McRFPy_Automation::_keyUp(PyObject* self, PyObject* args) {
const char* keyName;
if (!PyArg_ParseTuple(args, "s", &keyName)) {
return NULL;
}
sf::Keyboard::Key key = stringToKey(keyName);
if (key == sf::Keyboard::Unknown) {
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
return NULL;
}
injectKeyEvent(sf::Event::KeyReleased, key);
Py_RETURN_NONE;
}
// Hotkey combination
PyObject* McRFPy_Automation::_hotkey(PyObject* self, PyObject* args) {
// Get all keys as separate arguments
Py_ssize_t numKeys = PyTuple_Size(args);
if (numKeys == 0) {
PyErr_SetString(PyExc_ValueError, "hotkey() requires at least one key");
return NULL;
}
// Press all keys
for (Py_ssize_t i = 0; i < numKeys; i++) {
PyObject* keyObj = PyTuple_GetItem(args, i);
const char* keyName = PyUnicode_AsUTF8(keyObj);
if (!keyName) {
return NULL;
}
sf::Keyboard::Key key = stringToKey(keyName);
if (key == sf::Keyboard::Unknown) {
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
return NULL;
}
injectKeyEvent(sf::Event::KeyPressed, key);
sleep_ms(10); // Small delay between key presses
}
// Release all keys in reverse order
for (Py_ssize_t i = numKeys - 1; i >= 0; i--) {
PyObject* keyObj = PyTuple_GetItem(args, i);
const char* keyName = PyUnicode_AsUTF8(keyObj);
sf::Keyboard::Key key = stringToKey(keyName);
injectKeyEvent(sf::Event::KeyReleased, key);
sleep_ms(10);
}
Py_RETURN_NONE;
}
// Scroll wheel
PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"clicks", "x", "y", NULL};
int clicks;
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|ii", const_cast<char**>(kwlist),
&clicks, &x, &y)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
// Inject scroll event
injectMouseEvent(sf::Event::MouseWheelScrolled, clicks, y);
Py_RETURN_NONE;
}
// Other click types using the main click function
PyObject* McRFPy_Automation::_middleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("middle"));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
PyObject* McRFPy_Automation::_tripleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(3));
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
// Mouse button press/release
PyObject* McRFPy_Automation::_mouseDown(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "button", NULL};
int x = -1, y = -1;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
&x, &y, &button)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
sf::Mouse::Button sfButton = sf::Mouse::Left;
if (strcmp(button, "right") == 0) {
sfButton = sf::Mouse::Right;
} else if (strcmp(button, "middle") == 0) {
sfButton = sf::Mouse::Middle;
}
injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton);
Py_RETURN_NONE;
}
PyObject* McRFPy_Automation::_mouseUp(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "button", NULL};
int x = -1, y = -1;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
&x, &y, &button)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
sf::Mouse::Button sfButton = sf::Mouse::Left;
if (strcmp(button, "right") == 0) {
sfButton = sf::Mouse::Right;
} else if (strcmp(button, "middle") == 0) {
sfButton = sf::Mouse::Middle;
}
injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton);
Py_RETURN_NONE;
}
// Drag operations
PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "duration", "button", NULL};
int x, y;
float duration = 0.0f;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
&x, &y, &duration, &button)) {
return NULL;
}
// Get current position
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
int startX, startY;
if (!PyArg_ParseTuple(pos, "ii", &startX, &startY)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
// Mouse down at current position
PyObject* downArgs = Py_BuildValue("(ii)", startX, startY);
PyObject* downKwargs = PyDict_New();
PyDict_SetItemString(downKwargs, "button", PyUnicode_FromString(button));
PyObject* downResult = _mouseDown(self, downArgs, downKwargs);
Py_DECREF(downArgs);
Py_DECREF(downKwargs);
if (!downResult) return NULL;
Py_DECREF(downResult);
// Move to target position
if (duration > 0) {
// Smooth movement
int steps = static_cast<int>(duration * 60); // 60 FPS
for (int i = 1; i <= steps; i++) {
int currentX = startX + (x - startX) * i / steps;
int currentY = startY + (y - startY) * i / steps;
injectMouseEvent(sf::Event::MouseMoved, currentX, currentY);
sleep_ms(1000 / 60); // 60 FPS
}
} else {
injectMouseEvent(sf::Event::MouseMoved, x, y);
}
// Mouse up at target position
PyObject* upArgs = Py_BuildValue("(ii)", x, y);
PyObject* upKwargs = PyDict_New();
PyDict_SetItemString(upKwargs, "button", PyUnicode_FromString(button));
PyObject* upResult = _mouseUp(self, upArgs, upKwargs);
Py_DECREF(upArgs);
Py_DECREF(upKwargs);
if (!upResult) return NULL;
Py_DECREF(upResult);
Py_RETURN_NONE;
}
PyObject* McRFPy_Automation::_dragRel(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"xOffset", "yOffset", "duration", "button", NULL};
int xOffset, yOffset;
float duration = 0.0f;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
&xOffset, &yOffset, &duration, &button)) {
return NULL;
}
// Get current position
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
int currentX, currentY;
if (!PyArg_ParseTuple(pos, "ii", &currentX, &currentY)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
// Call dragTo with absolute position
PyObject* dragArgs = Py_BuildValue("(ii)", currentX + xOffset, currentY + yOffset);
PyObject* dragKwargs = PyDict_New();
PyDict_SetItemString(dragKwargs, "duration", PyFloat_FromDouble(duration));
PyDict_SetItemString(dragKwargs, "button", PyUnicode_FromString(button));
PyObject* result = _dragTo(self, dragArgs, dragKwargs);
Py_DECREF(dragArgs);
Py_DECREF(dragKwargs);
return result;
}
// Method definitions for the automation module
static PyMethodDef automationMethods[] = {
{"screenshot", McRFPy_Automation::_screenshot, METH_VARARGS,
"screenshot(filename) - Save a screenshot to the specified file"},
{"position", McRFPy_Automation::_position, METH_NOARGS,
"position() - Get current mouse position as (x, y) tuple"},
{"size", McRFPy_Automation::_size, METH_NOARGS,
"size() - Get screen size as (width, height) tuple"},
{"onScreen", McRFPy_Automation::_onScreen, METH_VARARGS,
"onScreen(x, y) - Check if coordinates are within screen bounds"},
{"moveTo", (PyCFunction)McRFPy_Automation::_moveTo, METH_VARARGS | METH_KEYWORDS,
"moveTo(x, y, duration=0.0) - Move mouse to absolute position"},
{"moveRel", (PyCFunction)McRFPy_Automation::_moveRel, METH_VARARGS | METH_KEYWORDS,
"moveRel(xOffset, yOffset, duration=0.0) - Move mouse relative to current position"},
{"dragTo", (PyCFunction)McRFPy_Automation::_dragTo, METH_VARARGS | METH_KEYWORDS,
"dragTo(x, y, duration=0.0, button='left') - Drag mouse to position"},
{"dragRel", (PyCFunction)McRFPy_Automation::_dragRel, METH_VARARGS | METH_KEYWORDS,
"dragRel(xOffset, yOffset, duration=0.0, button='left') - Drag mouse relative to current position"},
{"click", (PyCFunction)McRFPy_Automation::_click, METH_VARARGS | METH_KEYWORDS,
"click(x=None, y=None, clicks=1, interval=0.0, button='left') - Click at position"},
{"rightClick", (PyCFunction)McRFPy_Automation::_rightClick, METH_VARARGS | METH_KEYWORDS,
"rightClick(x=None, y=None) - Right click at position"},
{"middleClick", (PyCFunction)McRFPy_Automation::_middleClick, METH_VARARGS | METH_KEYWORDS,
"middleClick(x=None, y=None) - Middle click at position"},
{"doubleClick", (PyCFunction)McRFPy_Automation::_doubleClick, METH_VARARGS | METH_KEYWORDS,
"doubleClick(x=None, y=None) - Double click at position"},
{"tripleClick", (PyCFunction)McRFPy_Automation::_tripleClick, METH_VARARGS | METH_KEYWORDS,
"tripleClick(x=None, y=None) - Triple click at position"},
{"scroll", (PyCFunction)McRFPy_Automation::_scroll, METH_VARARGS | METH_KEYWORDS,
"scroll(clicks, x=None, y=None) - Scroll wheel at position"},
{"mouseDown", (PyCFunction)McRFPy_Automation::_mouseDown, METH_VARARGS | METH_KEYWORDS,
"mouseDown(x=None, y=None, button='left') - Press mouse button"},
{"mouseUp", (PyCFunction)McRFPy_Automation::_mouseUp, METH_VARARGS | METH_KEYWORDS,
"mouseUp(x=None, y=None, button='left') - Release mouse button"},
{"typewrite", (PyCFunction)McRFPy_Automation::_typewrite, METH_VARARGS | METH_KEYWORDS,
"typewrite(message, interval=0.0) - Type text with optional interval between keystrokes"},
{"hotkey", McRFPy_Automation::_hotkey, METH_VARARGS,
"hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c'))"},
{"keyDown", McRFPy_Automation::_keyDown, METH_VARARGS,
"keyDown(key) - Press and hold a key"},
{"keyUp", McRFPy_Automation::_keyUp, METH_VARARGS,
"keyUp(key) - Release a key"},
{NULL, NULL, 0, NULL}
};
// Module definition for mcrfpy.automation
static PyModuleDef automationModule = {
PyModuleDef_HEAD_INIT,
"mcrfpy.automation",
"Automation API for McRogueFace - PyAutoGUI-compatible interface",
-1,
automationMethods
};
// Initialize automation submodule
PyObject* McRFPy_Automation::init_automation_module() {
PyObject* module = PyModule_Create(&automationModule);
if (module == NULL) {
return NULL;
}
return module;
}

View File

@ -1,62 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <SFML/Graphics.hpp>
#include <SFML/Window.hpp>
#include <string>
#include <chrono>
#include <thread>
class GameEngine;
class McRFPy_Automation {
public:
// Initialize the automation submodule
static PyObject* init_automation_module();
// Screenshot functionality
static PyObject* _screenshot(PyObject* self, PyObject* args);
// Mouse position and screen info
static PyObject* _position(PyObject* self, PyObject* args);
static PyObject* _size(PyObject* self, PyObject* args);
static PyObject* _onScreen(PyObject* self, PyObject* args);
// Mouse movement
static PyObject* _moveTo(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _moveRel(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _dragTo(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _dragRel(PyObject* self, PyObject* args, PyObject* kwargs);
// Mouse clicks
static PyObject* _click(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _rightClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _middleClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _doubleClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _tripleClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _scroll(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _mouseDown(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _mouseUp(PyObject* self, PyObject* args, PyObject* kwargs);
// Keyboard
static PyObject* _typewrite(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _hotkey(PyObject* self, PyObject* args);
static PyObject* _keyDown(PyObject* self, PyObject* args);
static PyObject* _keyUp(PyObject* self, PyObject* args);
// Helper functions
static void injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button = sf::Mouse::Left);
static void injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key);
static void injectTextEvent(sf::Uint32 unicode);
static sf::Keyboard::Key stringToKey(const std::string& keyName);
static void sleep_ms(int milliseconds);
// #111 - Simulated mouse position for headless mode
static sf::Vector2i getSimulatedMousePosition();
private:
static GameEngine* getGameEngine();
// #111 - Track simulated mouse position for headless mode
static sf::Vector2i simulated_mouse_pos;
};

View File

@ -1,31 +0,0 @@
#ifndef MCRFPY_DOC_H
#define MCRFPY_DOC_H
// Section builders for documentation
#define MCRF_SIG(params, ret) params " -> " ret "\n\n"
#define MCRF_DESC(text) text "\n\n"
#define MCRF_ARGS_START "Args:\n"
#define MCRF_ARG(name, desc) " " name ": " desc "\n"
#define MCRF_RETURNS(text) "\nReturns:\n " text "\n"
#define MCRF_RAISES(exc, desc) "\nRaises:\n " exc ": " desc "\n"
#define MCRF_NOTE(text) "\nNote:\n " text "\n"
// Link to external documentation
// Format: MCRF_LINK("docs/file.md", "Link Text")
// Parsers detect this pattern and format per output type
#define MCRF_LINK(ref, text) "\nSee also: " text " (" ref ")\n"
// Main documentation macros
#define MCRF_METHOD_DOC(name, sig, desc, ...) \
name sig desc __VA_ARGS__
#define MCRF_FUNCTION(name, ...) \
MCRF_METHOD_DOC(#name, __VA_ARGS__)
#define MCRF_METHOD(cls, name, ...) \
MCRF_METHOD_DOC(#name, __VA_ARGS__)
#define MCRF_PROPERTY(name, desc) \
desc
#endif // MCRFPY_DOC_H

View File

@ -1,324 +0,0 @@
#include "McRFPy_Libtcod.h"
#include "McRFPy_API.h"
#include "UIGrid.h"
#include <vector>
// Helper function to get UIGrid from Python object
static UIGrid* get_grid_from_pyobject(PyObject* obj) {
auto grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
if (!grid_type) {
PyErr_SetString(PyExc_RuntimeError, "Could not find Grid type");
return nullptr;
}
if (!PyObject_IsInstance(obj, (PyObject*)grid_type)) {
Py_DECREF(grid_type);
PyErr_SetString(PyExc_TypeError, "First argument must be a Grid object");
return nullptr;
}
Py_DECREF(grid_type);
PyUIGridObject* pygrid = (PyUIGridObject*)obj;
return pygrid->data.get();
}
// Field of View computation
static PyObject* McRFPy_Libtcod::compute_fov(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y, radius;
int light_walls = 1;
int algorithm = FOV_BASIC;
if (!PyArg_ParseTuple(args, "Oiii|ii", &grid_obj, &x, &y, &radius,
&light_walls, &algorithm)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// Compute FOV using grid's method
grid->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
// Return list of visible cells
PyObject* visible_list = PyList_New(0);
for (int gy = 0; gy < grid->grid_y; gy++) {
for (int gx = 0; gx < grid->grid_x; gx++) {
if (grid->isInFOV(gx, gy)) {
PyObject* pos = Py_BuildValue("(ii)", gx, gy);
PyList_Append(visible_list, pos);
Py_DECREF(pos);
}
}
}
return visible_list;
}
// A* Pathfinding
static PyObject* McRFPy_Libtcod::find_path(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x1, y1, x2, y2;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTuple(args, "Oiiii|f", &grid_obj, &x1, &y1, &x2, &y2, &diagonal_cost)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// Get path from grid
std::vector<std::pair<int, int>> path = grid->findPath(x1, y1, x2, y2, diagonal_cost);
// Convert to Python list
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // steals reference
}
return path_list;
}
// Line drawing algorithm
static PyObject* McRFPy_Libtcod::line(PyObject* self, PyObject* args) {
int x1, y1, x2, y2;
if (!PyArg_ParseTuple(args, "iiii", &x1, &y1, &x2, &y2)) {
return NULL;
}
// Use TCOD's line algorithm
TCODLine::init(x1, y1, x2, y2);
PyObject* line_list = PyList_New(0);
int x, y;
// Step through line
while (!TCODLine::step(&x, &y)) {
PyObject* pos = Py_BuildValue("(ii)", x, y);
PyList_Append(line_list, pos);
Py_DECREF(pos);
}
return line_list;
}
// Line iterator (generator-like function)
static PyObject* McRFPy_Libtcod::line_iter(PyObject* self, PyObject* args) {
// For simplicity, just call line() for now
// A proper implementation would create an iterator object
return line(self, args);
}
// Dijkstra pathfinding
static PyObject* McRFPy_Libtcod::dijkstra_new(PyObject* self, PyObject* args) {
PyObject* grid_obj;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTuple(args, "O|f", &grid_obj, &diagonal_cost)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// For now, just return the grid object since Dijkstra is part of the grid
Py_INCREF(grid_obj);
return grid_obj;
}
static PyObject* McRFPy_Libtcod::dijkstra_compute(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int root_x, root_y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &root_x, &root_y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
grid->computeDijkstra(root_x, root_y);
Py_RETURN_NONE;
}
static PyObject* McRFPy_Libtcod::dijkstra_get_distance(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
float distance = grid->getDijkstraDistance(x, y);
if (distance < 0) {
Py_RETURN_NONE;
}
return PyFloat_FromDouble(distance);
}
static PyObject* McRFPy_Libtcod::dijkstra_path_to(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
std::vector<std::pair<int, int>> path = grid->getDijkstraPath(x, y);
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // steals reference
}
return path_list;
}
// Add FOV algorithm constants to module
static PyObject* McRFPy_Libtcod::add_fov_constants(PyObject* module) {
// FOV algorithms
PyModule_AddIntConstant(module, "FOV_BASIC", FOV_BASIC);
PyModule_AddIntConstant(module, "FOV_DIAMOND", FOV_DIAMOND);
PyModule_AddIntConstant(module, "FOV_SHADOW", FOV_SHADOW);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_0", FOV_PERMISSIVE_0);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_1", FOV_PERMISSIVE_1);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_2", FOV_PERMISSIVE_2);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_3", FOV_PERMISSIVE_3);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_4", FOV_PERMISSIVE_4);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_5", FOV_PERMISSIVE_5);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_6", FOV_PERMISSIVE_6);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_7", FOV_PERMISSIVE_7);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_8", FOV_PERMISSIVE_8);
PyModule_AddIntConstant(module, "FOV_RESTRICTIVE", FOV_RESTRICTIVE);
PyModule_AddIntConstant(module, "FOV_SYMMETRIC_SHADOWCAST", FOV_SYMMETRIC_SHADOWCAST);
return module;
}
// Method definitions
static PyMethodDef libtcodMethods[] = {
{"compute_fov", McRFPy_Libtcod::compute_fov, METH_VARARGS,
"compute_fov(grid, x, y, radius, light_walls=True, algorithm=FOV_BASIC)\n\n"
"Compute field of view from a position.\n\n"
"Args:\n"
" grid: Grid object to compute FOV on\n"
" x, y: Origin position\n"
" radius: Maximum sight radius\n"
" light_walls: Whether walls are lit when in FOV\n"
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_SHADOW, etc.)\n\n"
"Returns:\n"
" List of (x, y) tuples for visible cells"},
{"find_path", McRFPy_Libtcod::find_path, METH_VARARGS,
"find_path(grid, x1, y1, x2, y2, diagonal_cost=1.41)\n\n"
"Find shortest path between two points using A*.\n\n"
"Args:\n"
" grid: Grid object to pathfind on\n"
" x1, y1: Starting position\n"
" x2, y2: Target position\n"
" diagonal_cost: Cost of diagonal movement\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, or empty list if no path exists"},
{"line", McRFPy_Libtcod::line, METH_VARARGS,
"line(x1, y1, x2, y2)\n\n"
"Get cells along a line using Bresenham's algorithm.\n\n"
"Args:\n"
" x1, y1: Starting position\n"
" x2, y2: Ending position\n\n"
"Returns:\n"
" List of (x, y) tuples along the line"},
{"line_iter", McRFPy_Libtcod::line_iter, METH_VARARGS,
"line_iter(x1, y1, x2, y2)\n\n"
"Iterate over cells along a line.\n\n"
"Args:\n"
" x1, y1: Starting position\n"
" x2, y2: Ending position\n\n"
"Returns:\n"
" Iterator of (x, y) tuples along the line"},
{"dijkstra_new", McRFPy_Libtcod::dijkstra_new, METH_VARARGS,
"dijkstra_new(grid, diagonal_cost=1.41)\n\n"
"Create a Dijkstra pathfinding context for a grid.\n\n"
"Args:\n"
" grid: Grid object to use for pathfinding\n"
" diagonal_cost: Cost of diagonal movement\n\n"
"Returns:\n"
" Grid object configured for Dijkstra pathfinding"},
{"dijkstra_compute", McRFPy_Libtcod::dijkstra_compute, METH_VARARGS,
"dijkstra_compute(grid, root_x, root_y)\n\n"
"Compute Dijkstra distance map from root position.\n\n"
"Args:\n"
" grid: Grid object with Dijkstra context\n"
" root_x, root_y: Root position to compute distances from"},
{"dijkstra_get_distance", McRFPy_Libtcod::dijkstra_get_distance, METH_VARARGS,
"dijkstra_get_distance(grid, x, y)\n\n"
"Get distance from root to a position.\n\n"
"Args:\n"
" grid: Grid object with computed Dijkstra map\n"
" x, y: Position to get distance for\n\n"
"Returns:\n"
" Float distance or None if position is invalid/unreachable"},
{"dijkstra_path_to", McRFPy_Libtcod::dijkstra_path_to, METH_VARARGS,
"dijkstra_path_to(grid, x, y)\n\n"
"Get shortest path from position to Dijkstra root.\n\n"
"Args:\n"
" grid: Grid object with computed Dijkstra map\n"
" x, y: Starting position\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path to root"},
{NULL, NULL, 0, NULL}
};
// Module definition
static PyModuleDef libtcodModule = {
PyModuleDef_HEAD_INIT,
"mcrfpy.libtcod",
"TCOD-compatible algorithms for field of view, pathfinding, and line drawing.\n\n"
"This module provides access to TCOD's algorithms integrated with McRogueFace grids.\n"
"Unlike the original TCOD, these functions work directly with Grid objects.\n\n"
"FOV Algorithms:\n"
" FOV_BASIC - Basic circular FOV\n"
" FOV_SHADOW - Shadow casting (recommended)\n"
" FOV_DIAMOND - Diamond-shaped FOV\n"
" FOV_PERMISSIVE_0 through FOV_PERMISSIVE_8 - Permissive variants\n"
" FOV_RESTRICTIVE - Most restrictive FOV\n"
" FOV_SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\n"
"Example:\n"
" import mcrfpy\n"
" from mcrfpy import libtcod\n\n"
" grid = mcrfpy.Grid(50, 50)\n"
" visible = libtcod.compute_fov(grid, 25, 25, 10)\n"
" path = libtcod.find_path(grid, 0, 0, 49, 49)",
-1,
libtcodMethods
};
// Module initialization
PyObject* McRFPy_Libtcod::init_libtcod_module() {
PyObject* m = PyModule_Create(&libtcodModule);
if (m == NULL) {
return NULL;
}
// Add FOV algorithm constants
add_fov_constants(m);
return m;
}

View File

@ -1,27 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <libtcod.h>
namespace McRFPy_Libtcod
{
// Field of View algorithms
static PyObject* compute_fov(PyObject* self, PyObject* args);
// Pathfinding
static PyObject* find_path(PyObject* self, PyObject* args);
static PyObject* dijkstra_new(PyObject* self, PyObject* args);
static PyObject* dijkstra_compute(PyObject* self, PyObject* args);
static PyObject* dijkstra_get_distance(PyObject* self, PyObject* args);
static PyObject* dijkstra_path_to(PyObject* self, PyObject* args);
// Line algorithms
static PyObject* line(PyObject* self, PyObject* args);
static PyObject* line_iter(PyObject* self, PyObject* args);
// FOV algorithm constants
static PyObject* add_fov_constants(PyObject* module);
// Module initialization
PyObject* init_libtcod_module();
}

View File

@ -1,40 +0,0 @@
#ifndef MCROGUEFACE_CONFIG_H
#define MCROGUEFACE_CONFIG_H
#include <string>
#include <vector>
#include <filesystem>
struct McRogueFaceConfig {
// McRogueFace specific
bool headless = false;
bool audio_enabled = true;
// Python interpreter emulation
bool python_mode = false;
std::string python_command; // -c command
std::string python_module; // -m module
bool interactive_mode = false; // -i flag
bool show_version = false; // -V flag
bool show_help = false; // -h flag
// Script execution
std::filesystem::path script_path;
std::vector<std::string> script_args;
// Scripts to execute before main script (--exec flag)
std::vector<std::filesystem::path> exec_scripts;
// Screenshot functionality for headless mode
std::string screenshot_path;
bool take_screenshot = false;
// Auto-exit when no timers remain (for --headless --exec automation)
bool auto_exit_after_exec = false;
// Exception handling: exit on first Python callback exception (default: true)
// Use --continue-after-exceptions to disable
bool exit_on_exception = true;
};
#endif // MCROGUEFACE_CONFIG_H

View File

@ -1,61 +0,0 @@
#include "Profiler.h"
#include <iostream>
ProfilingLogger::ProfilingLogger()
: headers_written(false)
{
}
ProfilingLogger::~ProfilingLogger() {
close();
}
bool ProfilingLogger::open(const std::string& filename, const std::vector<std::string>& columns) {
column_names = columns;
file.open(filename);
if (!file.is_open()) {
std::cerr << "Failed to open profiling log file: " << filename << std::endl;
return false;
}
// Write CSV header
for (size_t i = 0; i < columns.size(); ++i) {
file << columns[i];
if (i < columns.size() - 1) {
file << ",";
}
}
file << "\n";
file.flush();
headers_written = true;
return true;
}
void ProfilingLogger::writeRow(const std::vector<float>& values) {
if (!file.is_open()) {
return;
}
if (values.size() != column_names.size()) {
std::cerr << "ProfilingLogger: value count (" << values.size()
<< ") doesn't match column count (" << column_names.size() << ")" << std::endl;
return;
}
for (size_t i = 0; i < values.size(); ++i) {
file << values[i];
if (i < values.size() - 1) {
file << ",";
}
}
file << "\n";
}
void ProfilingLogger::close() {
if (file.is_open()) {
file.flush();
file.close();
}
}

View File

@ -1,111 +0,0 @@
#pragma once
#include <chrono>
#include <string>
#include <vector>
#include <fstream>
/**
* @brief Simple RAII-based profiling timer for measuring code execution time
*
* Usage:
* float timing = 0.0f;
* {
* ScopedTimer timer(timing);
* // ... code to profile ...
* } // timing now contains elapsed milliseconds
*/
class ScopedTimer {
private:
std::chrono::high_resolution_clock::time_point start;
float& target_ms;
public:
/**
* @brief Construct a new Scoped Timer and start timing
* @param target Reference to float that will receive elapsed time in milliseconds
*/
explicit ScopedTimer(float& target)
: target_ms(target)
{
start = std::chrono::high_resolution_clock::now();
}
/**
* @brief Destructor automatically records elapsed time
*/
~ScopedTimer() {
auto end = std::chrono::high_resolution_clock::now();
target_ms = std::chrono::duration<float, std::milli>(end - start).count();
}
// Prevent copying
ScopedTimer(const ScopedTimer&) = delete;
ScopedTimer& operator=(const ScopedTimer&) = delete;
};
/**
* @brief Accumulating timer that adds elapsed time to existing value
*
* Useful for measuring total time across multiple calls in a single frame
*/
class AccumulatingTimer {
private:
std::chrono::high_resolution_clock::time_point start;
float& target_ms;
public:
explicit AccumulatingTimer(float& target)
: target_ms(target)
{
start = std::chrono::high_resolution_clock::now();
}
~AccumulatingTimer() {
auto end = std::chrono::high_resolution_clock::now();
target_ms += std::chrono::duration<float, std::milli>(end - start).count();
}
AccumulatingTimer(const AccumulatingTimer&) = delete;
AccumulatingTimer& operator=(const AccumulatingTimer&) = delete;
};
/**
* @brief CSV profiling data logger for batch analysis
*
* Writes profiling data to CSV file for later analysis with Python/pandas/Excel
*/
class ProfilingLogger {
private:
std::ofstream file;
bool headers_written;
std::vector<std::string> column_names;
public:
ProfilingLogger();
~ProfilingLogger();
/**
* @brief Open a CSV file for writing profiling data
* @param filename Path to CSV file
* @param columns Column names for the CSV header
* @return true if file opened successfully
*/
bool open(const std::string& filename, const std::vector<std::string>& columns);
/**
* @brief Write a row of profiling data
* @param values Data values (must match column count)
*/
void writeRow(const std::vector<float>& values);
/**
* @brief Close the file and flush data
*/
void close();
/**
* @brief Check if logger is ready to write
*/
bool isOpen() const { return file.is_open(); }
};

View File

@ -1,135 +0,0 @@
#include "GameEngine.h"
#include <sstream>
#include <iomanip>
GameEngine::ProfilerOverlay::ProfilerOverlay(sf::Font& fontRef)
: font(fontRef), visible(false), updateInterval(10), frameCounter(0)
{
text.setFont(font);
text.setCharacterSize(14);
text.setFillColor(sf::Color::White);
text.setPosition(10.0f, 10.0f);
// Semi-transparent dark background
background.setFillColor(sf::Color(0, 0, 0, 180));
background.setPosition(5.0f, 5.0f);
}
void GameEngine::ProfilerOverlay::toggle() {
visible = !visible;
}
void GameEngine::ProfilerOverlay::setVisible(bool vis) {
visible = vis;
}
bool GameEngine::ProfilerOverlay::isVisible() const {
return visible;
}
sf::Color GameEngine::ProfilerOverlay::getPerformanceColor(float frameTimeMs) {
if (frameTimeMs < 16.6f) {
return sf::Color::Green; // 60+ FPS
} else if (frameTimeMs < 33.3f) {
return sf::Color::Yellow; // 30-60 FPS
} else {
return sf::Color::Red; // <30 FPS
}
}
std::string GameEngine::ProfilerOverlay::formatFloat(float value, int precision) {
std::stringstream ss;
ss << std::fixed << std::setprecision(precision) << value;
return ss.str();
}
std::string GameEngine::ProfilerOverlay::formatPercentage(float part, float total) {
if (total <= 0.0f) return "0%";
float pct = (part / total) * 100.0f;
return formatFloat(pct, 0) + "%";
}
void GameEngine::ProfilerOverlay::update(const ProfilingMetrics& metrics) {
if (!visible) return;
// Only update text every N frames to reduce overhead
frameCounter++;
if (frameCounter < updateInterval) {
return;
}
frameCounter = 0;
std::stringstream ss;
ss << "McRogueFace Performance Monitor\n";
ss << "================================\n";
// Frame time and FPS
float frameMs = metrics.avgFrameTime;
ss << "FPS: " << metrics.fps << " (" << formatFloat(frameMs, 1) << "ms/frame)\n";
// Performance warning
if (frameMs > 33.3f) {
ss << "WARNING: Frame time exceeds 30 FPS target!\n";
}
ss << "\n";
// Timing breakdown
ss << "Frame Time Breakdown:\n";
ss << " Grid Render: " << formatFloat(metrics.gridRenderTime, 1) << "ms ("
<< formatPercentage(metrics.gridRenderTime, frameMs) << ")\n";
ss << " Cells: " << metrics.gridCellsRendered << " rendered\n";
ss << " Entities: " << metrics.entitiesRendered << " / " << metrics.totalEntities << " drawn\n";
if (metrics.fovOverlayTime > 0.01f) {
ss << " FOV Overlay: " << formatFloat(metrics.fovOverlayTime, 1) << "ms\n";
}
if (metrics.entityRenderTime > 0.01f) {
ss << " Entity Render: " << formatFloat(metrics.entityRenderTime, 1) << "ms ("
<< formatPercentage(metrics.entityRenderTime, frameMs) << ")\n";
}
if (metrics.pythonScriptTime > 0.01f) {
ss << " Python: " << formatFloat(metrics.pythonScriptTime, 1) << "ms ("
<< formatPercentage(metrics.pythonScriptTime, frameMs) << ")\n";
}
if (metrics.animationTime > 0.01f) {
ss << " Animations: " << formatFloat(metrics.animationTime, 1) << "ms ("
<< formatPercentage(metrics.animationTime, frameMs) << ")\n";
}
ss << "\n";
// Other metrics
ss << "Draw Calls: " << metrics.drawCalls << "\n";
ss << "UI Elements: " << metrics.uiElements << " (" << metrics.visibleElements << " visible)\n";
// Calculate unaccounted time
float accountedTime = metrics.gridRenderTime + metrics.entityRenderTime +
metrics.pythonScriptTime + metrics.animationTime;
float unaccountedTime = frameMs - accountedTime;
if (unaccountedTime > 1.0f) {
ss << "\n";
ss << "Other: " << formatFloat(unaccountedTime, 1) << "ms ("
<< formatPercentage(unaccountedTime, frameMs) << ")\n";
}
ss << "\n";
ss << "Press F3 to hide this overlay";
text.setString(ss.str());
// Update background size to fit text
sf::FloatRect textBounds = text.getLocalBounds();
background.setSize(sf::Vector2f(textBounds.width + 20.0f, textBounds.height + 20.0f));
}
void GameEngine::ProfilerOverlay::render(sf::RenderTarget& target) {
if (!visible) return;
target.draw(background);
target.draw(text);
}

View File

@ -1,319 +0,0 @@
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include "UIDrawable.h"
#include "UIFrame.h"
#include "UICaption.h"
#include "UISprite.h"
#include "UIGrid.h"
#include "UIEntity.h"
#include "UI.h" // For the PyTypeObject definitions
#include <cstring>
PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyAnimationObject* self = (PyAnimationObject*)type->tp_alloc(type, 0);
if (self != NULL) {
// Will be initialized in init
}
return (PyObject*)self;
}
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr};
const char* property_name;
PyObject* target_value;
float duration;
const char* easing_name = "linear";
int delta = 0;
PyObject* callback = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_name, &delta, &callback)) {
return -1;
}
// Validate callback is callable if provided
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
PyErr_SetString(PyExc_TypeError, "callback must be callable");
return -1;
}
// Convert None to nullptr for C++
if (callback == Py_None) {
callback = nullptr;
}
// Convert Python target value to AnimationValue
AnimationValue animValue;
if (PyFloat_Check(target_value)) {
animValue = static_cast<float>(PyFloat_AsDouble(target_value));
}
else if (PyLong_Check(target_value)) {
animValue = static_cast<int>(PyLong_AsLong(target_value));
}
else if (PyList_Check(target_value)) {
// List of integers for sprite animation
std::vector<int> indices;
Py_ssize_t size = PyList_Size(target_value);
for (Py_ssize_t i = 0; i < size; i++) {
PyObject* item = PyList_GetItem(target_value, i);
if (PyLong_Check(item)) {
indices.push_back(PyLong_AsLong(item));
} else {
PyErr_SetString(PyExc_TypeError, "Sprite animation list must contain only integers");
return -1;
}
}
animValue = indices;
}
else if (PyTuple_Check(target_value)) {
Py_ssize_t size = PyTuple_Size(target_value);
if (size == 2) {
// Vector2f
float x = PyFloat_AsDouble(PyTuple_GetItem(target_value, 0));
float y = PyFloat_AsDouble(PyTuple_GetItem(target_value, 1));
animValue = sf::Vector2f(x, y);
}
else if (size == 3 || size == 4) {
// Color (RGB or RGBA)
int r = PyLong_AsLong(PyTuple_GetItem(target_value, 0));
int g = PyLong_AsLong(PyTuple_GetItem(target_value, 1));
int b = PyLong_AsLong(PyTuple_GetItem(target_value, 2));
int a = size == 4 ? PyLong_AsLong(PyTuple_GetItem(target_value, 3)) : 255;
animValue = sf::Color(r, g, b, a);
}
else {
PyErr_SetString(PyExc_ValueError, "Tuple must have 2 elements (vector) or 3-4 elements (color)");
return -1;
}
}
else if (PyUnicode_Check(target_value)) {
// String for text animation
const char* str = PyUnicode_AsUTF8(target_value);
animValue = std::string(str);
}
else {
PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string");
return -1;
}
// Get easing function
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
// Create the Animation
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
return 0;
}
void PyAnimation::dealloc(PyAnimationObject* self) {
self->data.reset();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* PyAnimation::get_property(PyAnimationObject* self, void* closure) {
return PyUnicode_FromString(self->data->getTargetProperty().c_str());
}
PyObject* PyAnimation::get_duration(PyAnimationObject* self, void* closure) {
return PyFloat_FromDouble(self->data->getDuration());
}
PyObject* PyAnimation::get_elapsed(PyAnimationObject* self, void* closure) {
return PyFloat_FromDouble(self->data->getElapsed());
}
PyObject* PyAnimation::get_is_complete(PyAnimationObject* self, void* closure) {
return PyBool_FromLong(self->data->isComplete());
}
PyObject* PyAnimation::get_is_delta(PyAnimationObject* self, void* closure) {
return PyBool_FromLong(self->data->isDelta());
}
PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
PyObject* target_obj;
if (!PyArg_ParseTuple(args, "O", &target_obj)) {
return NULL;
}
// Get type objects from the module to ensure they're initialized
PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
PyObject* caption_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
PyObject* sprite_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
PyObject* grid_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
PyObject* entity_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
bool handled = false;
// Use PyObject_IsInstance to support inheritance
if (frame_type && PyObject_IsInstance(target_obj, frame_type)) {
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
if (frame->data) {
self->data->start(frame->data);
AnimationManager::getInstance().addAnimation(self->data);
handled = true;
}
}
else if (caption_type && PyObject_IsInstance(target_obj, caption_type)) {
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
if (caption->data) {
self->data->start(caption->data);
AnimationManager::getInstance().addAnimation(self->data);
handled = true;
}
}
else if (sprite_type && PyObject_IsInstance(target_obj, sprite_type)) {
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
if (sprite->data) {
self->data->start(sprite->data);
AnimationManager::getInstance().addAnimation(self->data);
handled = true;
}
}
else if (grid_type && PyObject_IsInstance(target_obj, grid_type)) {
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
if (grid->data) {
self->data->start(grid->data);
AnimationManager::getInstance().addAnimation(self->data);
handled = true;
}
}
else if (entity_type && PyObject_IsInstance(target_obj, entity_type)) {
// Special handling for Entity since it doesn't inherit from UIDrawable
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
if (entity->data) {
self->data->startEntity(entity->data);
AnimationManager::getInstance().addAnimation(self->data);
handled = true;
}
}
// Clean up references
Py_XDECREF(frame_type);
Py_XDECREF(caption_type);
Py_XDECREF(sprite_type);
Py_XDECREF(grid_type);
Py_XDECREF(entity_type);
if (!handled) {
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity (or a subclass of these)");
return NULL;
}
Py_RETURN_NONE;
}
PyObject* PyAnimation::update(PyAnimationObject* self, PyObject* args) {
float deltaTime;
if (!PyArg_ParseTuple(args, "f", &deltaTime)) {
return NULL;
}
bool still_running = self->data->update(deltaTime);
return PyBool_FromLong(still_running);
}
PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args) {
AnimationValue value = self->data->getCurrentValue();
// Convert AnimationValue back to Python
return std::visit([](const auto& val) -> PyObject* {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
return PyFloat_FromDouble(val);
}
else if constexpr (std::is_same_v<T, int>) {
return PyLong_FromLong(val);
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// This shouldn't happen as we interpolate to int
return PyLong_FromLong(0);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
return Py_BuildValue("(iiii)", val.r, val.g, val.b, val.a);
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
return Py_BuildValue("(ff)", val.x, val.y);
}
else if constexpr (std::is_same_v<T, std::string>) {
return PyUnicode_FromString(val.c_str());
}
Py_RETURN_NONE;
}, value);
}
PyObject* PyAnimation::complete(PyAnimationObject* self, PyObject* args) {
if (self->data) {
self->data->complete();
}
Py_RETURN_NONE;
}
PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) {
if (self->data && self->data->hasValidTarget()) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
}
PyGetSetDef PyAnimation::getsetters[] = {
{"property", (getter)get_property, NULL,
MCRF_PROPERTY(property, "Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index')."), NULL},
{"duration", (getter)get_duration, NULL,
MCRF_PROPERTY(duration, "Animation duration in seconds (float, read-only). Total time for the animation to complete."), NULL},
{"elapsed", (getter)get_elapsed, NULL,
MCRF_PROPERTY(elapsed, "Elapsed time in seconds (float, read-only). Time since the animation started."), NULL},
{"is_complete", (getter)get_is_complete, NULL,
MCRF_PROPERTY(is_complete, "Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called."), NULL},
{"is_delta", (getter)get_is_delta, NULL,
MCRF_PROPERTY(is_delta, "Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value."), NULL},
{NULL}
};
PyMethodDef PyAnimation::methods[] = {
{"start", (PyCFunction)start, METH_VARARGS,
MCRF_METHOD(Animation, start,
MCRF_SIG("(target: UIDrawable)", "None"),
MCRF_DESC("Start the animation on a target UI element."),
MCRF_ARGS_START
MCRF_ARG("target", "The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)")
MCRF_RETURNS("None")
MCRF_NOTE("The animation will automatically stop if the target is destroyed. Call AnimationManager.update(delta_time) each frame to progress animations.")
)},
{"update", (PyCFunction)update, METH_VARARGS,
MCRF_METHOD(Animation, update,
MCRF_SIG("(delta_time: float)", "bool"),
MCRF_DESC("Update the animation by the given time delta."),
MCRF_ARGS_START
MCRF_ARG("delta_time", "Time elapsed since last update in seconds")
MCRF_RETURNS("bool: True if animation is still running, False if complete")
MCRF_NOTE("Typically called by AnimationManager automatically. Manual calls only needed for custom animation control.")
)},
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
MCRF_METHOD(Animation, get_current_value,
MCRF_SIG("()", "Any"),
MCRF_DESC("Get the current interpolated value of the animation."),
MCRF_RETURNS("Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str)")
MCRF_NOTE("Return type matches the target property type. For sprite_index returns int, for pos returns (x, y), for fill_color returns (r, g, b, a).")
)},
{"complete", (PyCFunction)complete, METH_NOARGS,
MCRF_METHOD(Animation, complete,
MCRF_SIG("()", "None"),
MCRF_DESC("Complete the animation immediately by jumping to the final value."),
MCRF_RETURNS("None")
MCRF_NOTE("Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.")
)},
{"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS,
MCRF_METHOD(Animation, hasValidTarget,
MCRF_SIG("()", "bool"),
MCRF_DESC("Check if the animation still has a valid target."),
MCRF_RETURNS("bool: True if the target still exists, False if it was destroyed")
MCRF_NOTE("Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed.")
)},
{NULL}
};

View File

@ -1,52 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "structmember.h"
#include "Animation.h"
#include <memory>
typedef struct {
PyObject_HEAD
std::shared_ptr<Animation> data;
} PyAnimationObject;
class PyAnimation {
public:
static PyObject* create(PyTypeObject* type, PyObject* args, PyObject* kwds);
static int init(PyAnimationObject* self, PyObject* args, PyObject* kwds);
static void dealloc(PyAnimationObject* self);
// Properties
static PyObject* get_property(PyAnimationObject* self, void* closure);
static PyObject* get_duration(PyAnimationObject* self, void* closure);
static PyObject* get_elapsed(PyAnimationObject* self, void* closure);
static PyObject* get_is_complete(PyAnimationObject* self, void* closure);
static PyObject* get_is_delta(PyAnimationObject* self, void* closure);
// Methods
static PyObject* start(PyAnimationObject* self, PyObject* args);
static PyObject* update(PyAnimationObject* self, PyObject* args);
static PyObject* get_current_value(PyAnimationObject* self, PyObject* args);
static PyObject* complete(PyAnimationObject* self, PyObject* args);
static PyObject* has_valid_target(PyAnimationObject* self, PyObject* args);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
static PyTypeObject PyAnimationType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Animation",
.tp_basicsize = sizeof(PyAnimationObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyAnimation::dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Animation object for animating UI properties"),
.tp_methods = PyAnimation::methods,
.tp_getset = PyAnimation::getsetters,
.tp_init = (initproc)PyAnimation::init,
.tp_new = PyAnimation::create,
};
}

View File

@ -1,27 +1,10 @@
#include "PyCallable.h" #include "PyCallable.h"
#include "McRFPy_API.h"
#include "GameEngine.h"
PyCallable::PyCallable(PyObject* _target) PyCallable::PyCallable(PyObject* _target)
{ {
target = Py_XNewRef(_target); target = Py_XNewRef(_target);
} }
PyCallable::PyCallable(const PyCallable& other)
{
target = Py_XNewRef(other.target);
}
PyCallable& PyCallable::operator=(const PyCallable& other)
{
if (this != &other) {
PyObject* old_target = target;
target = Py_XNewRef(other.target);
Py_XDECREF(old_target);
}
return *this;
}
PyCallable::~PyCallable() PyCallable::~PyCallable()
{ {
if (target) if (target)
@ -33,11 +16,49 @@ PyObject* PyCallable::call(PyObject* args, PyObject* kwargs)
return PyObject_Call(target, args, kwargs); return PyObject_Call(target, args, kwargs);
} }
bool PyCallable::isNone() const bool PyCallable::isNone()
{ {
return (target == Py_None || target == NULL); return (target == Py_None || target == NULL);
} }
PyTimerCallable::PyTimerCallable(PyObject* _target, int _interval, int now)
: PyCallable(_target), interval(_interval), last_ran(now)
{}
PyTimerCallable::PyTimerCallable()
: PyCallable(Py_None), interval(0), last_ran(0)
{}
bool PyTimerCallable::hasElapsed(int now)
{
return now >= last_ran + interval;
}
void PyTimerCallable::call(int now)
{
PyObject* args = Py_BuildValue("(i)", now);
PyObject* retval = PyCallable::call(args, NULL);
if (!retval)
{
PyErr_Print();
PyErr_Clear();
} else if (retval != Py_None)
{
std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
std::cout << PyUnicode_AsUTF8(PyObject_Repr(retval)) << std::endl;
}
}
bool PyTimerCallable::test(int now)
{
if(hasElapsed(now))
{
call(now);
last_ran = now;
return true;
}
return false;
}
PyClickCallable::PyClickCallable(PyObject* _target) PyClickCallable::PyClickCallable(PyObject* _target)
: PyCallable(_target) : PyCallable(_target)
@ -53,14 +74,9 @@ void PyClickCallable::call(sf::Vector2f mousepos, std::string button, std::strin
PyObject* retval = PyCallable::call(args, NULL); PyObject* retval = PyCallable::call(args, NULL);
if (!retval) if (!retval)
{ {
std::cerr << "Click callback raised an exception:" << std::endl; std::cout << "ClickCallable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print(); PyErr_Print();
PyErr_Clear(); PyErr_Clear();
// Check if we should exit on exception
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
McRFPy_API::signalPythonException();
}
} else if (retval != Py_None) } else if (retval != Py_None)
{ {
std::cout << "ClickCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; std::cout << "ClickCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
@ -88,14 +104,9 @@ void PyKeyCallable::call(std::string key, std::string action)
PyObject* retval = PyCallable::call(args, NULL); PyObject* retval = PyCallable::call(args, NULL);
if (!retval) if (!retval)
{ {
std::cerr << "Key callback raised an exception:" << std::endl; std::cout << "KeyCallable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print(); PyErr_Print();
PyErr_Clear(); PyErr_Clear();
// Check if we should exit on exception
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
McRFPy_API::signalPythonException();
}
} else if (retval != Py_None) } else if (retval != Py_None)
{ {
std::cout << "KeyCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; std::cout << "KeyCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;

View File

@ -6,15 +6,24 @@ class PyCallable
{ {
protected: protected:
PyObject* target; PyObject* target;
public:
PyCallable(PyObject*); PyCallable(PyObject*);
PyCallable(const PyCallable& other);
PyCallable& operator=(const PyCallable& other);
~PyCallable(); ~PyCallable();
PyObject* call(PyObject*, PyObject*); PyObject* call(PyObject*, PyObject*);
bool isNone() const; public:
PyObject* borrow() const { return target; } bool isNone();
};
class PyTimerCallable: public PyCallable
{
private:
int interval;
int last_ran;
void call(int);
public:
bool hasElapsed(int);
bool test(int);
PyTimerCallable(PyObject*, int, int);
PyTimerCallable();
}; };
class PyClickCallable: public PyCallable class PyClickCallable: public PyCallable
@ -24,11 +33,6 @@ public:
PyObject* borrow(); PyObject* borrow();
PyClickCallable(PyObject*); PyClickCallable(PyObject*);
PyClickCallable(); PyClickCallable();
PyClickCallable(const PyClickCallable& other) : PyCallable(other) {}
PyClickCallable& operator=(const PyClickCallable& other) {
PyCallable::operator=(other);
return *this;
}
}; };
class PyKeyCallable: public PyCallable class PyKeyCallable: public PyCallable

View File

@ -1,51 +1,11 @@
#include "PyColor.h" #include "PyColor.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PyObjectUtils.h"
#include "PyRAII.h"
#include "McRFPy_Doc.h"
#include <string>
#include <cstdio>
PyGetSetDef PyColor::getsetters[] = { PyGetSetDef PyColor::getsetters[] = {
{"r", (getter)PyColor::get_member, (setter)PyColor::set_member, {"r", (getter)PyColor::get_member, (setter)PyColor::set_member, "Red component", (void*)0},
MCRF_PROPERTY(r, "Red component (0-255). Automatically clamped to valid range."), (void*)0}, {"g", (getter)PyColor::get_member, (setter)PyColor::set_member, "Green component", (void*)1},
{"g", (getter)PyColor::get_member, (setter)PyColor::set_member, {"b", (getter)PyColor::get_member, (setter)PyColor::set_member, "Blue component", (void*)2},
MCRF_PROPERTY(g, "Green component (0-255). Automatically clamped to valid range."), (void*)1}, {"a", (getter)PyColor::get_member, (setter)PyColor::set_member, "Alpha component", (void*)3},
{"b", (getter)PyColor::get_member, (setter)PyColor::set_member,
MCRF_PROPERTY(b, "Blue component (0-255). Automatically clamped to valid range."), (void*)2},
{"a", (getter)PyColor::get_member, (setter)PyColor::set_member,
MCRF_PROPERTY(a, "Alpha component (0-255, where 0=transparent, 255=opaque). Automatically clamped to valid range."), (void*)3},
{NULL}
};
PyMethodDef PyColor::methods[] = {
{"from_hex", (PyCFunction)PyColor::from_hex, METH_VARARGS | METH_CLASS,
MCRF_METHOD(Color, from_hex,
MCRF_SIG("(hex_string: str)", "Color"),
MCRF_DESC("Create a Color from a hexadecimal string."),
MCRF_ARGS_START
MCRF_ARG("hex_string", "Hex color string (e.g., '#FF0000', 'FF0000', '#AABBCCDD' for RGBA)")
MCRF_RETURNS("Color: New Color object with values from hex string")
MCRF_RAISES("ValueError", "If hex string is not 6 or 8 characters (RGB or RGBA)")
MCRF_NOTE("This is a class method. Call as Color.from_hex('#FF0000')")
)},
{"to_hex", (PyCFunction)PyColor::to_hex, METH_NOARGS,
MCRF_METHOD(Color, to_hex,
MCRF_SIG("()", "str"),
MCRF_DESC("Convert this Color to a hexadecimal string."),
MCRF_RETURNS("str: Hex string in format '#RRGGBB' or '#RRGGBBAA' (if alpha < 255)")
MCRF_NOTE("Alpha component is only included if not fully opaque (< 255)")
)},
{"lerp", (PyCFunction)PyColor::lerp, METH_VARARGS,
MCRF_METHOD(Color, lerp,
MCRF_SIG("(other: Color, t: float)", "Color"),
MCRF_DESC("Linearly interpolate between this color and another."),
MCRF_ARGS_START
MCRF_ARG("other", "The target Color to interpolate towards")
MCRF_ARG("t", "Interpolation factor (0.0 = this color, 1.0 = other color). Automatically clamped to [0.0, 1.0]")
MCRF_RETURNS("Color: New Color representing the interpolated value")
MCRF_NOTE("All components (r, g, b, a) are interpolated independently")
)},
{NULL} {NULL}
}; };
@ -54,16 +14,11 @@ PyColor::PyColor(sf::Color target)
PyObject* PyColor::pyObject() PyObject* PyColor::pyObject()
{ {
PyTypeObject* type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"); PyObject* obj = PyType_GenericAlloc(&mcrfpydef::PyColorType, 0);
if (!type) return nullptr; Py_INCREF(obj);
PyColorObject* self = (PyColorObject*)obj;
PyColorObject* obj = (PyColorObject*)type->tp_alloc(type, 0); self->data = data;
Py_DECREF(type); return obj;
if (obj) {
obj->data = data;
}
return (PyObject*)obj;
} }
sf::Color PyColor::fromPy(PyObject* obj) sf::Color PyColor::fromPy(PyObject* obj)
@ -171,190 +126,25 @@ PyObject* PyColor::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
PyObject* PyColor::get_member(PyObject* obj, void* closure) PyObject* PyColor::get_member(PyObject* obj, void* closure)
{ {
PyColorObject* self = (PyColorObject*)obj; // TODO
long member = (long)closure; return Py_None;
switch (member) {
case 0: // r
return PyLong_FromLong(self->data.r);
case 1: // g
return PyLong_FromLong(self->data.g);
case 2: // b
return PyLong_FromLong(self->data.b);
case 3: // a
return PyLong_FromLong(self->data.a);
default:
PyErr_SetString(PyExc_AttributeError, "Invalid color member");
return NULL;
}
} }
int PyColor::set_member(PyObject* obj, PyObject* value, void* closure) int PyColor::set_member(PyObject* obj, PyObject* value, void* closure)
{ {
PyColorObject* self = (PyColorObject*)obj; // TODO
long member = (long)closure;
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "Color values must be integers");
return -1;
}
long val = PyLong_AsLong(value);
if (val < 0 || val > 255) {
PyErr_SetString(PyExc_ValueError, "Color values must be between 0 and 255");
return -1;
}
switch (member) {
case 0: // r
self->data.r = static_cast<sf::Uint8>(val);
break;
case 1: // g
self->data.g = static_cast<sf::Uint8>(val);
break;
case 2: // b
self->data.b = static_cast<sf::Uint8>(val);
break;
case 3: // a
self->data.a = static_cast<sf::Uint8>(val);
break;
default:
PyErr_SetString(PyExc_AttributeError, "Invalid color member");
return -1;
}
return 0; return 0;
} }
PyColorObject* PyColor::from_arg(PyObject* args) PyColorObject* PyColor::from_arg(PyObject* args)
{ {
// Use RAII for type reference management
PyRAII::PyTypeRef type("Color", McRFPy_API::mcrf_module);
if (!type) {
return NULL;
}
// Check if args is already a Color instance
if (PyObject_IsInstance(args, (PyObject*)type.get())) {
Py_INCREF(args); // Return new reference so caller can safely DECREF
return (PyColorObject*)args;
}
// Create new Color object using RAII
PyRAII::PyObjectRef obj(type->tp_alloc(type.get(), 0), true);
if (!obj) {
return NULL;
}
// Initialize the object
int err = init((PyColorObject*)obj.get(), args, NULL);
if (err) {
// obj will be automatically cleaned up when it goes out of scope
return NULL;
}
// Release ownership and return
return (PyColorObject*)obj.release();
}
// Color helper method implementations
PyObject* PyColor::from_hex(PyObject* cls, PyObject* args)
{
const char* hex_str;
if (!PyArg_ParseTuple(args, "s", &hex_str)) {
return NULL;
}
std::string hex(hex_str);
// Remove # if present
if (hex.length() > 0 && hex[0] == '#') {
hex = hex.substr(1);
}
// Validate hex string
if (hex.length() != 6 && hex.length() != 8) {
PyErr_SetString(PyExc_ValueError, "Hex string must be 6 or 8 characters (RGB or RGBA)");
return NULL;
}
// Parse hex values
try {
unsigned int r = std::stoul(hex.substr(0, 2), nullptr, 16);
unsigned int g = std::stoul(hex.substr(2, 2), nullptr, 16);
unsigned int b = std::stoul(hex.substr(4, 2), nullptr, 16);
unsigned int a = 255;
if (hex.length() == 8) {
a = std::stoul(hex.substr(6, 2), nullptr, 16);
}
// Create new Color object
PyTypeObject* type = (PyTypeObject*)cls;
PyColorObject* color = (PyColorObject*)type->tp_alloc(type, 0);
if (color) {
color->data = sf::Color(r, g, b, a);
}
return (PyObject*)color;
} catch (const std::exception& e) {
PyErr_SetString(PyExc_ValueError, "Invalid hex string");
return NULL;
}
}
PyObject* PyColor::to_hex(PyColorObject* self, PyObject* Py_UNUSED(ignored))
{
char hex[10]; // #RRGGBBAA + null terminator
// Include alpha only if not fully opaque
if (self->data.a < 255) {
snprintf(hex, sizeof(hex), "#%02X%02X%02X%02X",
self->data.r, self->data.g, self->data.b, self->data.a);
} else {
snprintf(hex, sizeof(hex), "#%02X%02X%02X",
self->data.r, self->data.g, self->data.b);
}
return PyUnicode_FromString(hex);
}
PyObject* PyColor::lerp(PyColorObject* self, PyObject* args)
{
PyObject* other_obj;
float t;
if (!PyArg_ParseTuple(args, "Of", &other_obj, &t)) {
return NULL;
}
// Validate other color
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"); auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
if (!PyObject_IsInstance(other_obj, (PyObject*)type)) { if (PyObject_IsInstance(args, (PyObject*)type)) return (PyColorObject*)args;
Py_DECREF(type); auto obj = (PyColorObject*)type->tp_alloc(type, 0);
PyErr_SetString(PyExc_TypeError, "First argument must be a Color"); int err = init(obj, args, NULL);
if (err) {
Py_DECREF(obj);
return NULL; return NULL;
} }
return obj;
PyColorObject* other = (PyColorObject*)other_obj;
// Clamp t to [0, 1]
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
// Perform linear interpolation
sf::Uint8 r = static_cast<sf::Uint8>(self->data.r + (other->data.r - self->data.r) * t);
sf::Uint8 g = static_cast<sf::Uint8>(self->data.g + (other->data.g - self->data.g) * t);
sf::Uint8 b = static_cast<sf::Uint8>(self->data.b + (other->data.b - self->data.b) * t);
sf::Uint8 a = static_cast<sf::Uint8>(self->data.a + (other->data.a - self->data.a) * t);
// Create new Color object
PyColorObject* result = (PyColorObject*)type->tp_alloc(type, 0);
Py_DECREF(type);
if (result) {
result->data = sf::Color(r, g, b, a);
}
return (PyObject*)result;
} }

View File

@ -28,19 +28,12 @@ public:
static PyObject* get_member(PyObject*, void*); static PyObject* get_member(PyObject*, void*);
static int set_member(PyObject*, PyObject*, void*); static int set_member(PyObject*, PyObject*, void*);
// Color helper methods
static PyObject* from_hex(PyObject* cls, PyObject* args);
static PyObject* to_hex(PyColorObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* lerp(PyColorObject* self, PyObject* args);
static PyGetSetDef getsetters[]; static PyGetSetDef getsetters[];
static PyMethodDef methods[];
static PyColorObject* from_arg(PyObject*); static PyColorObject* from_arg(PyObject*);
}; };
namespace mcrfpydef { namespace mcrfpydef {
static PyTypeObject PyColorType = { static PyTypeObject PyColorType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Color", .tp_name = "mcrfpy.Color",
.tp_basicsize = sizeof(PyColorObject), .tp_basicsize = sizeof(PyColorObject),
.tp_itemsize = 0, .tp_itemsize = 0,
@ -48,7 +41,6 @@ namespace mcrfpydef {
.tp_hash = PyColor::hash, .tp_hash = PyColor::hash,
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Color Object"), .tp_doc = PyDoc_STR("SFML Color Object"),
.tp_methods = PyColor::methods,
.tp_getset = PyColor::getsetters, .tp_getset = PyColor::getsetters,
.tp_init = (initproc)PyColor::init, .tp_init = (initproc)PyColor::init,
.tp_new = PyColor::pynew, .tp_new = PyColor::pynew,

View File

@ -1,211 +0,0 @@
#include "PyDrawable.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
// Click property getter
static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure)
{
if (!self->data->click_callable)
Py_RETURN_NONE;
PyObject* ptr = self->data->click_callable->borrow();
if (ptr && ptr != Py_None)
return ptr;
else
Py_RETURN_NONE;
}
// Click property setter
static int PyDrawable_set_click(PyDrawableObject* self, PyObject* value, void* closure)
{
if (value == Py_None) {
self->data->click_unregister();
} else if (PyCallable_Check(value)) {
self->data->click_register(value);
} else {
PyErr_SetString(PyExc_TypeError, "click must be callable or None");
return -1;
}
return 0;
}
// Z-index property getter
static PyObject* PyDrawable_get_z_index(PyDrawableObject* self, void* closure)
{
return PyLong_FromLong(self->data->z_index);
}
// Z-index property setter
static int PyDrawable_set_z_index(PyDrawableObject* self, PyObject* value, void* closure)
{
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
return -1;
}
int val = PyLong_AsLong(value);
self->data->z_index = val;
// Mark scene as needing resort
self->data->notifyZIndexChanged();
return 0;
}
// Visible property getter (new for #87)
static PyObject* PyDrawable_get_visible(PyDrawableObject* self, void* closure)
{
return PyBool_FromLong(self->data->visible);
}
// Visible property setter (new for #87)
static int PyDrawable_set_visible(PyDrawableObject* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
self->data->visible = (value == Py_True);
return 0;
}
// Opacity property getter (new for #88)
static PyObject* PyDrawable_get_opacity(PyDrawableObject* self, void* closure)
{
return PyFloat_FromDouble(self->data->opacity);
}
// Opacity property setter (new for #88)
static int PyDrawable_set_opacity(PyDrawableObject* self, PyObject* value, void* closure)
{
float val;
if (PyFloat_Check(value)) {
val = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
val = PyLong_AsLong(value);
} else {
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
return -1;
}
// Clamp to valid range
if (val < 0.0f) val = 0.0f;
if (val > 1.0f) val = 1.0f;
self->data->opacity = val;
return 0;
}
// GetSetDef array for properties
static PyGetSetDef PyDrawable_getsetters[] = {
{"on_click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click,
MCRF_PROPERTY(on_click,
"Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click."
), NULL},
{"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index,
MCRF_PROPERTY(z_index,
"Z-order for rendering (lower values rendered first). "
"Automatically triggers scene resort when changed."
), NULL},
{"visible", (getter)PyDrawable_get_visible, (setter)PyDrawable_set_visible,
MCRF_PROPERTY(visible,
"Whether the object is visible (bool). "
"Invisible objects are not rendered or clickable."
), NULL},
{"opacity", (getter)PyDrawable_get_opacity, (setter)PyDrawable_set_opacity,
MCRF_PROPERTY(opacity,
"Opacity level (0.0 = transparent, 1.0 = opaque). "
"Automatically clamped to valid range [0.0, 1.0]."
), NULL},
{NULL} // Sentinel
};
// get_bounds method implementation (#89)
static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUSED(args))
{
auto bounds = self->data->get_bounds();
return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height);
}
// move method implementation (#98)
static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args)
{
float dx, dy;
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) {
return NULL;
}
self->data->move(dx, dy);
Py_RETURN_NONE;
}
// resize method implementation (#98)
static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args)
{
float w, h;
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
return NULL;
}
self->data->resize(w, h);
Py_RETURN_NONE;
}
// Method definitions
static PyMethodDef PyDrawable_methods[] = {
{"get_bounds", (PyCFunction)PyDrawable_get_bounds, METH_NOARGS,
MCRF_METHOD(Drawable, get_bounds,
MCRF_SIG("()", "tuple"),
MCRF_DESC("Get the bounding rectangle of this drawable element."),
MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds")
MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.")
)},
{"move", (PyCFunction)PyDrawable_move, METH_VARARGS,
MCRF_METHOD(Drawable, move,
MCRF_SIG("(dx: float, dy: float)", "None"),
MCRF_DESC("Move the element by a relative offset."),
MCRF_ARGS_START
MCRF_ARG("dx", "Horizontal offset in pixels")
MCRF_ARG("dy", "Vertical offset in pixels")
MCRF_NOTE("This modifies the x and y position properties by the given amounts.")
)},
{"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS,
MCRF_METHOD(Drawable, resize,
MCRF_SIG("(width: float, height: float)", "None"),
MCRF_DESC("Resize the element to new dimensions."),
MCRF_ARGS_START
MCRF_ARG("width", "New width in pixels")
MCRF_ARG("height", "New height in pixels")
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.")
)},
{NULL} // Sentinel
};
// Type initialization
static int PyDrawable_init(PyDrawableObject* self, PyObject* args, PyObject* kwds)
{
PyErr_SetString(PyExc_TypeError, "Drawable is an abstract base class and cannot be instantiated directly");
return -1;
}
namespace mcrfpydef {
PyTypeObject PyDrawableType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Drawable",
.tp_basicsize = sizeof(PyDrawableObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self) {
PyDrawableObject* obj = (PyDrawableObject*)self;
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Base class for all drawable UI elements"),
.tp_methods = PyDrawable_methods,
.tp_getset = PyDrawable_getsetters,
.tp_init = (initproc)PyDrawable_init,
.tp_new = PyType_GenericNew,
};
}

View File

@ -1,15 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "UIDrawable.h"
// Python object structure for UIDrawable base class
typedef struct {
PyObject_HEAD
std::shared_ptr<UIDrawable> data;
} PyDrawableObject;
// Declare the Python type for Drawable base class
namespace mcrfpydef {
extern PyTypeObject PyDrawableType;
}

View File

@ -1,6 +1,5 @@
#include "PyFont.h" #include "PyFont.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "McRFPy_Doc.h"
PyFont::PyFont(std::string filename) PyFont::PyFont(std::string filename)
@ -62,21 +61,3 @@ PyObject* PyFont::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
{ {
return (PyObject*)type->tp_alloc(type, 0); return (PyObject*)type->tp_alloc(type, 0);
} }
PyObject* PyFont::get_family(PyFontObject* self, void* closure)
{
return PyUnicode_FromString(self->data->font.getInfo().family.c_str());
}
PyObject* PyFont::get_source(PyFontObject* self, void* closure)
{
return PyUnicode_FromString(self->data->source.c_str());
}
PyGetSetDef PyFont::getsetters[] = {
{"family", (getter)PyFont::get_family, NULL,
MCRF_PROPERTY(family, "Font family name (str, read-only). Retrieved from font metadata."), NULL},
{"source", (getter)PyFont::get_source, NULL,
MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this font."), NULL},
{NULL} // Sentinel
};

View File

@ -21,17 +21,10 @@ public:
static Py_hash_t hash(PyObject*); static Py_hash_t hash(PyObject*);
static int init(PyFontObject*, PyObject*, PyObject*); static int init(PyFontObject*, PyObject*, PyObject*);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL); static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
// Getters for properties
static PyObject* get_family(PyFontObject* self, void* closure);
static PyObject* get_source(PyFontObject* self, void* closure);
static PyGetSetDef getsetters[];
}; };
namespace mcrfpydef { namespace mcrfpydef {
static PyTypeObject PyFontType = { static PyTypeObject PyFontType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Font", .tp_name = "mcrfpy.Font",
.tp_basicsize = sizeof(PyFontObject), .tp_basicsize = sizeof(PyFontObject),
.tp_itemsize = 0, .tp_itemsize = 0,
@ -39,7 +32,6 @@ namespace mcrfpydef {
//.tp_hash = PyFont::hash, //.tp_hash = PyFont::hash,
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Font Object"), .tp_doc = PyDoc_STR("SFML Font Object"),
.tp_getset = PyFont::getsetters,
//.tp_base = &PyBaseObject_Type, //.tp_base = &PyBaseObject_Type,
.tp_init = (initproc)PyFont::init, .tp_init = (initproc)PyFont::init,
.tp_new = PyType_GenericNew, //PyFont::pynew, .tp_new = PyType_GenericNew, //PyFont::pynew,

View File

@ -1,76 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "McRFPy_API.h"
#include "PyRAII.h"
namespace PyObjectUtils {
// Template for getting Python type object from module
template<typename T>
PyTypeObject* getPythonType(const char* typeName) {
PyTypeObject* type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, typeName);
if (!type) {
PyErr_Format(PyExc_RuntimeError, "Could not find %s type in module", typeName);
}
return type;
}
// Generic function to create a Python object of given type
inline PyObject* createPyObjectGeneric(const char* typeName) {
PyTypeObject* type = getPythonType<void>(typeName);
if (!type) return nullptr;
PyObject* obj = type->tp_alloc(type, 0);
Py_DECREF(type);
return obj;
}
// Helper function to allocate and initialize a Python object with data
template<typename PyObjType, typename DataType>
PyObject* createPyObjectWithData(const char* typeName, DataType data) {
PyTypeObject* type = getPythonType<void>(typeName);
if (!type) return nullptr;
PyObjType* obj = (PyObjType*)type->tp_alloc(type, 0);
Py_DECREF(type);
if (obj) {
obj->data = data;
}
return (PyObject*)obj;
}
// Function to convert UIDrawable to appropriate Python object
// This is moved to UICollection.cpp to avoid circular dependencies
// RAII-based object creation example
inline PyObject* createPyObjectGenericRAII(const char* typeName) {
PyRAII::PyTypeRef type(typeName, McRFPy_API::mcrf_module);
if (!type) {
PyErr_Format(PyExc_RuntimeError, "Could not find %s type in module", typeName);
return nullptr;
}
PyObject* obj = type->tp_alloc(type.get(), 0);
// Return the new reference (caller owns it)
return obj;
}
// Example of using PyObjectRef for safer reference management
template<typename PyObjType, typename DataType>
PyObject* createPyObjectWithDataRAII(const char* typeName, DataType data) {
PyRAII::PyObjectRef obj = PyRAII::createObject<PyObjType>(typeName, McRFPy_API::mcrf_module);
if (!obj) {
PyErr_Format(PyExc_RuntimeError, "Could not create %s object", typeName);
return nullptr;
}
// Access the object through the RAII wrapper
((PyObjType*)obj.get())->data = data;
// Release ownership to return to Python
return obj.release();
}
}

View File

@ -1,164 +0,0 @@
#pragma once
#include "Python.h"
#include "PyVector.h"
#include "McRFPy_API.h"
// Helper class for standardized position argument parsing across UI classes
class PyPositionHelper {
public:
// Template structure for parsing results
struct ParseResult {
float x = 0.0f;
float y = 0.0f;
bool has_position = false;
};
struct ParseResultInt {
int x = 0;
int y = 0;
bool has_position = false;
};
// Parse position from multiple formats for UI class constructors
// Supports: (x, y), x=x, y=y, ((x,y)), (pos=(x,y)), (Vector), pos=Vector
static ParseResult parse_position(PyObject* args, PyObject* kwds,
int* arg_index = nullptr)
{
ParseResult result;
float x = 0.0f, y = 0.0f;
PyObject* pos_obj = nullptr;
int start_index = arg_index ? *arg_index : 0;
// Check for positional tuple (x, y) first
if (!kwds && PyTuple_Size(args) > start_index + 1) {
PyObject* first = PyTuple_GetItem(args, start_index);
PyObject* second = PyTuple_GetItem(args, start_index + 1);
// Check if both are numbers
if ((PyFloat_Check(first) || PyLong_Check(first)) &&
(PyFloat_Check(second) || PyLong_Check(second))) {
x = PyFloat_Check(first) ? PyFloat_AsDouble(first) : PyLong_AsLong(first);
y = PyFloat_Check(second) ? PyFloat_AsDouble(second) : PyLong_AsLong(second);
result.x = x;
result.y = y;
result.has_position = true;
if (arg_index) *arg_index += 2;
return result;
}
}
// Check for single positional argument that might be tuple or Vector
if (!kwds && PyTuple_Size(args) > start_index) {
PyObject* first = PyTuple_GetItem(args, start_index);
PyVectorObject* vec = PyVector::from_arg(first);
if (vec) {
result.x = vec->data.x;
result.y = vec->data.y;
result.has_position = true;
if (arg_index) *arg_index += 1;
return result;
}
}
// Try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_kw = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj) {
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
}
if (pos_kw) {
PyVectorObject* vec = PyVector::from_arg(pos_kw);
if (vec) {
result.x = vec->data.x;
result.y = vec->data.y;
result.has_position = true;
return result;
}
}
}
return result;
}
// Parse integer position for Grid.at() and similar
static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds)
{
ParseResultInt result;
// Check for positional tuple (x, y) first
if (!kwds && PyTuple_Size(args) >= 2) {
PyObject* first = PyTuple_GetItem(args, 0);
PyObject* second = PyTuple_GetItem(args, 1);
if (PyLong_Check(first) && PyLong_Check(second)) {
result.x = PyLong_AsLong(first);
result.y = PyLong_AsLong(second);
result.has_position = true;
return result;
}
}
// Check for single tuple argument
if (!kwds && PyTuple_Size(args) == 1) {
PyObject* first = PyTuple_GetItem(args, 0);
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
PyObject* x_obj = PyTuple_GetItem(first, 0);
PyObject* y_obj = PyTuple_GetItem(first, 1);
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
result.x = PyLong_AsLong(x_obj);
result.y = PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
}
}
// Try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj && PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
result.x = PyLong_AsLong(x_obj);
result.y = PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
if (pos_obj && PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if (PyLong_Check(x_val) && PyLong_Check(y_val)) {
result.x = PyLong_AsLong(x_val);
result.y = PyLong_AsLong(y_val);
result.has_position = true;
return result;
}
}
}
return result;
}
// Error message helper
static void set_position_error() {
PyErr_SetString(PyExc_TypeError,
"Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), or pos=Vector");
}
static void set_position_int_error() {
PyErr_SetString(PyExc_TypeError,
"Position must be specified as: (x, y), x=x, y=y, ((x,y)), or pos=(x,y) with integer values");
}
};

View File

@ -1,138 +0,0 @@
#pragma once
#include "Python.h"
#include <utility>
namespace PyRAII {
// RAII wrapper for PyObject* that automatically manages reference counting
class PyObjectRef {
private:
PyObject* ptr;
public:
// Constructors
PyObjectRef() : ptr(nullptr) {}
explicit PyObjectRef(PyObject* p, bool steal_ref = false) : ptr(p) {
if (ptr && !steal_ref) {
Py_INCREF(ptr);
}
}
// Copy constructor
PyObjectRef(const PyObjectRef& other) : ptr(other.ptr) {
if (ptr) {
Py_INCREF(ptr);
}
}
// Move constructor
PyObjectRef(PyObjectRef&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
// Destructor
~PyObjectRef() {
Py_XDECREF(ptr);
}
// Copy assignment
PyObjectRef& operator=(const PyObjectRef& other) {
if (this != &other) {
Py_XDECREF(ptr);
ptr = other.ptr;
if (ptr) {
Py_INCREF(ptr);
}
}
return *this;
}
// Move assignment
PyObjectRef& operator=(PyObjectRef&& other) noexcept {
if (this != &other) {
Py_XDECREF(ptr);
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
// Access operators
PyObject* get() const { return ptr; }
PyObject* operator->() const { return ptr; }
PyObject& operator*() const { return *ptr; }
operator bool() const { return ptr != nullptr; }
// Release ownership (for returning to Python)
PyObject* release() {
PyObject* temp = ptr;
ptr = nullptr;
return temp;
}
// Reset with new pointer
void reset(PyObject* p = nullptr, bool steal_ref = false) {
if (p != ptr) {
Py_XDECREF(ptr);
ptr = p;
if (ptr && !steal_ref) {
Py_INCREF(ptr);
}
}
}
};
// Helper class for managing PyTypeObject* references from module lookups
class PyTypeRef {
private:
PyTypeObject* type;
public:
PyTypeRef() : type(nullptr) {}
explicit PyTypeRef(const char* typeName, PyObject* module) {
type = (PyTypeObject*)PyObject_GetAttrString(module, typeName);
// GetAttrString returns a new reference, so we own it
}
~PyTypeRef() {
Py_XDECREF((PyObject*)type);
}
// Delete copy operations to prevent accidental reference issues
PyTypeRef(const PyTypeRef&) = delete;
PyTypeRef& operator=(const PyTypeRef&) = delete;
// Allow move operations
PyTypeRef(PyTypeRef&& other) noexcept : type(other.type) {
other.type = nullptr;
}
PyTypeRef& operator=(PyTypeRef&& other) noexcept {
if (this != &other) {
Py_XDECREF((PyObject*)type);
type = other.type;
other.type = nullptr;
}
return *this;
}
PyTypeObject* get() const { return type; }
PyTypeObject* operator->() const { return type; }
operator bool() const { return type != nullptr; }
};
// Convenience function to create a new object with RAII
template<typename PyObjType>
PyObjectRef createObject(const char* typeName, PyObject* module) {
PyTypeRef type(typeName, module);
if (!type) {
return PyObjectRef();
}
PyObject* obj = type->tp_alloc(type.get(), 0);
// tp_alloc returns a new reference, so we steal it
return PyObjectRef(obj, true);
}
}

View File

@ -2,11 +2,6 @@
#include "ActionCode.h" #include "ActionCode.h"
#include "Resources.h" #include "Resources.h"
#include "PyCallable.h" #include "PyCallable.h"
#include "UIFrame.h"
#include "UIGrid.h"
#include "McRFPy_Automation.h" // #111 - For simulated mouse position
#include <algorithm>
#include <functional>
PyScene::PyScene(GameEngine* g) : Scene(g) PyScene::PyScene(GameEngine* g) : Scene(g)
{ {
@ -16,8 +11,7 @@ PyScene::PyScene(GameEngine* g) : Scene(g)
registerAction(ActionCode::MOUSEWHEEL + ActionCode::WHEEL_DEL, "wheel_up"); registerAction(ActionCode::MOUSEWHEEL + ActionCode::WHEEL_DEL, "wheel_up");
registerAction(ActionCode::MOUSEWHEEL + ActionCode::WHEEL_NEG + ActionCode::WHEEL_DEL, "wheel_down"); registerAction(ActionCode::MOUSEWHEEL + ActionCode::WHEEL_NEG + ActionCode::WHEEL_DEL, "wheel_down");
// console (` / ~ key) - don't hard code. registerAction(ActionCode::KEY + sf::Keyboard::Grave, "debug_menu");
//registerAction(ActionCode::KEY + sf::Keyboard::Grave, "debug_menu");
} }
void PyScene::update() void PyScene::update()
@ -26,42 +20,38 @@ void PyScene::update()
void PyScene::do_mouse_input(std::string button, std::string type) void PyScene::do_mouse_input(std::string button, std::string type)
{ {
sf::Vector2f mousepos;
// #111 - In headless mode, use simulated mouse position
if (game->isHeadless()) {
sf::Vector2i simPos = McRFPy_Automation::getSimulatedMousePosition();
mousepos = sf::Vector2f(static_cast<float>(simPos.x), static_cast<float>(simPos.y));
} else {
auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow()); auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow());
// Convert window coordinates to game coordinates using the viewport auto mousepos = game->getWindow().mapPixelToCoords(unscaledmousepos);
mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos)); UIDrawable* target;
for (auto d: *ui_elements)
{
target = d->click_at(sf::Vector2f(mousepos));
if (target)
{
/*
PyObject* args = Py_BuildValue("(iiss)", (int)mousepos.x, (int)mousepos.y, button.c_str(), type.c_str());
PyObject* retval = PyObject_Call(target->click_callable, args, NULL);
if (!retval)
{
std::cout << "click_callable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else if (retval != Py_None)
{
std::cout << "click_callable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
} }
*/
// Only sort if z_index values have changed
if (ui_elements_need_sort) {
// Sort in ascending order (same as render)
std::sort(ui_elements->begin(), ui_elements->end(),
[](const auto& a, const auto& b) { return a->z_index < b->z_index; });
ui_elements_need_sort = false;
}
// Check elements in reverse z-order (highest z_index first, top to bottom)
// Use reverse iterators to go from end to beginning
for (auto it = ui_elements->rbegin(); it != ui_elements->rend(); ++it) {
const auto& element = *it;
if (!element->visible) continue;
if (auto target = element->click_at(sf::Vector2f(mousepos))) {
target->click_callable->call(mousepos, button, type); target->click_callable->call(mousepos, button, type);
return; // Stop after first handler
} }
} }
} }
void PyScene::doAction(std::string name, std::string type) void PyScene::doAction(std::string name, std::string type)
{ {
if (name.compare("left") == 0 || name.compare("rclick") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) { if (ACTIONPY) {
McRFPy_API::doAction(name.substr(0, name.size() - 3));
}
else if (name.compare("left") == 0 || name.compare("rclick") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) {
do_mouse_input(name, type); do_mouse_input(name, type);
} }
else if ACTIONONCE("debug_menu") { else if ACTIONONCE("debug_menu") {
@ -69,119 +59,16 @@ void PyScene::doAction(std::string name, std::string type)
} }
} }
// #140 - Mouse enter/exit tracking
void PyScene::do_mouse_hover(int x, int y)
{
// In headless mode, use the coordinates directly (already in game space)
sf::Vector2f mousepos;
if (game->isHeadless()) {
mousepos = sf::Vector2f(static_cast<float>(x), static_cast<float>(y));
} else {
// Convert window coordinates to game coordinates using the viewport
mousepos = game->windowToGameCoords(sf::Vector2f(static_cast<float>(x), static_cast<float>(y)));
}
// Helper function to process hover for a single drawable and its children
std::function<void(UIDrawable*)> processHover = [&](UIDrawable* drawable) {
if (!drawable || !drawable->visible) return;
bool is_inside = drawable->contains_point(mousepos.x, mousepos.y);
bool was_hovered = drawable->hovered;
if (is_inside && !was_hovered) {
// Mouse entered
drawable->hovered = true;
if (drawable->on_enter_callable) {
drawable->on_enter_callable->call(mousepos, "enter", "start");
}
} else if (!is_inside && was_hovered) {
// Mouse exited
drawable->hovered = false;
if (drawable->on_exit_callable) {
drawable->on_exit_callable->call(mousepos, "exit", "start");
}
}
// #141 - Fire on_move if mouse is inside and has a move callback
if (is_inside && drawable->on_move_callable) {
drawable->on_move_callable->call(mousepos, "move", "start");
}
// Process children for Frame elements
if (drawable->derived_type() == PyObjectsEnum::UIFRAME) {
auto frame = static_cast<UIFrame*>(drawable);
if (frame->children) {
for (auto& child : *frame->children) {
processHover(child.get());
}
}
}
// Process children for Grid elements
else if (drawable->derived_type() == PyObjectsEnum::UIGRID) {
auto grid = static_cast<UIGrid*>(drawable);
// #142 - Update cell hover tracking for grid
grid->updateCellHover(mousepos);
if (grid->children) {
for (auto& child : *grid->children) {
processHover(child.get());
}
}
}
};
// Process all top-level UI elements
for (auto& element : *ui_elements) {
processHover(element.get());
}
}
void PyScene::render() void PyScene::render()
{ {
// #118: Skip rendering if scene is not visible game->getWindow().clear();
if (!visible) {
return;
}
game->getRenderTarget().clear(); auto vec = *ui_elements;
for (auto e: vec)
// Only sort if z_index values have changed
if (ui_elements_need_sort) {
std::sort(ui_elements->begin(), ui_elements->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
ui_elements_need_sort = false;
}
// Render in sorted order with scene-level transformations
for (auto e: *ui_elements)
{ {
if (e) { if (e)
// Track metrics e->render();
game->metrics.uiElements++;
if (e->visible) {
game->metrics.visibleElements++;
// Count this as a draw call (each visible element = 1+ draw calls)
game->metrics.drawCalls++;
} }
// #118: Apply scene-level opacity to element game->getWindow().display();
float original_opacity = e->opacity;
if (opacity < 1.0f) {
e->opacity = original_opacity * opacity;
}
// #118: Render with scene position offset
e->render(position, game->getRenderTarget());
// #118: Restore original opacity
if (opacity < 1.0f) {
e->opacity = original_opacity;
}
}
}
// Display is handled by GameEngine
} }

View File

@ -14,8 +14,4 @@ public:
void render() override final; void render() override final;
void do_mouse_input(std::string, std::string); void do_mouse_input(std::string, std::string);
void do_mouse_hover(int x, int y); // #140 - Mouse enter/exit tracking
// Dirty flag for z_index sorting optimization
bool ui_elements_need_sort = true;
}; };

View File

@ -1,464 +0,0 @@
#include "PySceneObject.h"
#include "PyScene.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include <iostream>
// Static map to store Python scene objects by name
static std::map<std::string, PySceneObject*> python_scenes;
PyObject* PySceneClass::__new__(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
PySceneObject* self = (PySceneObject*)type->tp_alloc(type, 0);
if (self) {
self->initialized = false;
// Don't create C++ scene yet - wait for __init__
}
return (PyObject*)self;
}
int PySceneClass::__init__(PySceneObject* self, PyObject* args, PyObject* kwds)
{
static const char* keywords[] = {"name", nullptr};
const char* name = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char**>(keywords), &name)) {
return -1;
}
// Check if scene with this name already exists
if (python_scenes.count(name) > 0) {
PyErr_Format(PyExc_ValueError, "Scene with name '%s' already exists", name);
return -1;
}
self->name = name;
// Create the C++ PyScene
McRFPy_API::game->createScene(name);
// Get reference to the created scene
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
// Store this Python object in our registry
python_scenes[name] = self;
Py_INCREF(self); // Keep a reference
// Create a Python function that routes to on_keypress
// We'll register this after the object is fully initialized
self->initialized = true;
return 0;
}
void PySceneClass::__dealloc(PyObject* self_obj)
{
PySceneObject* self = (PySceneObject*)self_obj;
// Remove from registry
if (python_scenes.count(self->name) > 0 && python_scenes[self->name] == self) {
python_scenes.erase(self->name);
}
// Call Python object destructor
Py_TYPE(self)->tp_free(self);
}
PyObject* PySceneClass::__repr__(PySceneObject* self)
{
return PyUnicode_FromFormat("<Scene '%s'>", self->name.c_str());
}
PyObject* PySceneClass::activate(PySceneObject* self, PyObject* args)
{
// Call the static method from McRFPy_API
PyObject* py_args = Py_BuildValue("(s)", self->name.c_str());
PyObject* result = McRFPy_API::_setScene(NULL, py_args);
Py_DECREF(py_args);
return result;
}
PyObject* PySceneClass::get_ui(PySceneObject* self, PyObject* args)
{
// Call the static method from McRFPy_API
PyObject* py_args = Py_BuildValue("(s)", self->name.c_str());
PyObject* result = McRFPy_API::_sceneUI(NULL, py_args);
Py_DECREF(py_args);
return result;
}
PyObject* PySceneClass::register_keyboard(PySceneObject* self, PyObject* args)
{
PyObject* callable;
if (!PyArg_ParseTuple(args, "O", &callable)) {
return NULL;
}
if (!PyCallable_Check(callable)) {
PyErr_SetString(PyExc_TypeError, "Argument must be callable");
return NULL;
}
// Store the callable
Py_INCREF(callable);
// Get the current scene and set its key_callable
GameEngine* game = McRFPy_API::game;
if (game) {
// We need to be on the right scene first
std::string old_scene = game->scene;
game->scene = self->name;
game->currentScene()->key_callable = std::make_unique<PyKeyCallable>(callable);
game->scene = old_scene;
}
Py_DECREF(callable);
Py_RETURN_NONE;
}
PyObject* PySceneClass::get_name(PySceneObject* self, void* closure)
{
return PyUnicode_FromString(self->name.c_str());
}
PyObject* PySceneClass::get_active(PySceneObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
Py_RETURN_FALSE;
}
return PyBool_FromLong(game->scene == self->name);
}
// #118: Scene position getter
static PyObject* PySceneClass_get_pos(PySceneObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
Py_RETURN_NONE;
}
// Get the scene by name using the public accessor
auto scene = game->getScene(self->name);
if (!scene) {
Py_RETURN_NONE;
}
// Create a Vector object
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (!type) return NULL;
PyObject* args = Py_BuildValue("(ff)", scene->position.x, scene->position.y);
PyObject* result = PyObject_CallObject((PyObject*)type, args);
Py_DECREF(type);
Py_DECREF(args);
return result;
}
// #118: Scene position setter
static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine");
return -1;
}
auto scene = game->getScene(self->name);
if (!scene) {
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
return -1;
}
// Accept tuple or Vector
float x, y;
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
x = PyFloat_AsDouble(PyTuple_GetItem(value, 0));
y = PyFloat_AsDouble(PyTuple_GetItem(value, 1));
} else if (PyObject_HasAttrString(value, "x") && PyObject_HasAttrString(value, "y")) {
PyObject* xobj = PyObject_GetAttrString(value, "x");
PyObject* yobj = PyObject_GetAttrString(value, "y");
x = PyFloat_AsDouble(xobj);
y = PyFloat_AsDouble(yobj);
Py_DECREF(xobj);
Py_DECREF(yobj);
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
return -1;
}
scene->position = sf::Vector2f(x, y);
return 0;
}
// #118: Scene visible getter
static PyObject* PySceneClass_get_visible(PySceneObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
Py_RETURN_TRUE;
}
auto scene = game->getScene(self->name);
if (!scene) {
Py_RETURN_TRUE;
}
return PyBool_FromLong(scene->visible);
}
// #118: Scene visible setter
static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine");
return -1;
}
auto scene = game->getScene(self->name);
if (!scene) {
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
return -1;
}
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
scene->visible = PyObject_IsTrue(value);
return 0;
}
// #118: Scene opacity getter
static PyObject* PySceneClass_get_opacity(PySceneObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
return PyFloat_FromDouble(1.0);
}
auto scene = game->getScene(self->name);
if (!scene) {
return PyFloat_FromDouble(1.0);
}
return PyFloat_FromDouble(scene->opacity);
}
// #118: Scene opacity setter
static int PySceneClass_set_opacity(PySceneObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine");
return -1;
}
auto scene = game->getScene(self->name);
if (!scene) {
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
return -1;
}
double opacity;
if (PyFloat_Check(value)) {
opacity = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
opacity = PyLong_AsDouble(value);
} else {
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
return -1;
}
// Clamp to valid range
if (opacity < 0.0) opacity = 0.0;
if (opacity > 1.0) opacity = 1.0;
scene->opacity = opacity;
return 0;
}
// Lifecycle callbacks
void PySceneClass::call_on_enter(PySceneObject* self)
{
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_enter");
if (method && PyCallable_Check(method)) {
PyObject* result = PyObject_CallNoArgs(method);
if (result) {
Py_DECREF(result);
} else {
PyErr_Print();
}
Py_DECREF(method);
} else {
// Clear AttributeError if method doesn't exist
PyErr_Clear();
Py_XDECREF(method);
}
}
void PySceneClass::call_on_exit(PySceneObject* self)
{
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_exit");
if (method && PyCallable_Check(method)) {
PyObject* result = PyObject_CallNoArgs(method);
if (result) {
Py_DECREF(result);
} else {
PyErr_Print();
}
Py_DECREF(method);
} else {
// Clear AttributeError if method doesn't exist
PyErr_Clear();
Py_XDECREF(method);
}
}
void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::string action)
{
PyGILState_STATE gstate = PyGILState_Ensure();
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_keypress");
if (method && PyCallable_Check(method)) {
PyObject* result = PyObject_CallFunction(method, "ss", key.c_str(), action.c_str());
if (result) {
Py_DECREF(result);
} else {
PyErr_Print();
}
Py_DECREF(method);
} else {
// Clear AttributeError if method doesn't exist
PyErr_Clear();
Py_XDECREF(method);
}
PyGILState_Release(gstate);
}
void PySceneClass::call_update(PySceneObject* self, float dt)
{
PyObject* method = PyObject_GetAttrString((PyObject*)self, "update");
if (method && PyCallable_Check(method)) {
PyObject* result = PyObject_CallFunction(method, "f", dt);
if (result) {
Py_DECREF(result);
} else {
PyErr_Print();
}
Py_DECREF(method);
} else {
// Clear AttributeError if method doesn't exist
PyErr_Clear();
Py_XDECREF(method);
}
}
void PySceneClass::call_on_resize(PySceneObject* self, int width, int height)
{
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_resize");
if (method && PyCallable_Check(method)) {
PyObject* result = PyObject_CallFunction(method, "ii", width, height);
if (result) {
Py_DECREF(result);
} else {
PyErr_Print();
}
Py_DECREF(method);
} else {
// Clear AttributeError if method doesn't exist
PyErr_Clear();
Py_XDECREF(method);
}
}
// Properties
PyGetSetDef PySceneClass::getsetters[] = {
{"name", (getter)get_name, NULL,
MCRF_PROPERTY(name, "Scene name (str, read-only). Unique identifier for this scene."), NULL},
{"active", (getter)get_active, NULL,
MCRF_PROPERTY(active, "Whether this scene is currently active (bool, read-only). Only one scene can be active at a time."), NULL},
// #118: Scene-level UIDrawable-like properties
{"pos", (getter)PySceneClass_get_pos, (setter)PySceneClass_set_pos,
MCRF_PROPERTY(pos, "Scene position offset (Vector). Applied to all UI elements during rendering."), NULL},
{"visible", (getter)PySceneClass_get_visible, (setter)PySceneClass_set_visible,
MCRF_PROPERTY(visible, "Scene visibility (bool). If False, scene is not rendered."), NULL},
{"opacity", (getter)PySceneClass_get_opacity, (setter)PySceneClass_set_opacity,
MCRF_PROPERTY(opacity, "Scene opacity (0.0-1.0). Applied to all UI elements during rendering."), NULL},
{NULL}
};
// Methods
PyMethodDef PySceneClass::methods[] = {
{"activate", (PyCFunction)activate, METH_NOARGS,
MCRF_METHOD(SceneClass, activate,
MCRF_SIG("()", "None"),
MCRF_DESC("Make this the active scene."),
MCRF_RETURNS("None")
MCRF_NOTE("Deactivates the current scene and activates this one. Scene transitions and lifecycle callbacks are triggered.")
)},
{"get_ui", (PyCFunction)get_ui, METH_NOARGS,
MCRF_METHOD(SceneClass, get_ui,
MCRF_SIG("()", "UICollection"),
MCRF_DESC("Get the UI element collection for this scene."),
MCRF_RETURNS("UICollection: Collection of UI elements (Frames, Captions, Sprites, Grids) in this scene")
MCRF_NOTE("Use to add, remove, or iterate over UI elements. Changes are reflected immediately.")
)},
{"register_keyboard", (PyCFunction)register_keyboard, METH_VARARGS,
MCRF_METHOD(SceneClass, register_keyboard,
MCRF_SIG("(callback: callable)", "None"),
MCRF_DESC("Register a keyboard event handler function."),
MCRF_ARGS_START
MCRF_ARG("callback", "Function that receives (key: str, pressed: bool) when keyboard events occur")
MCRF_RETURNS("None")
MCRF_NOTE("Alternative to overriding on_keypress() method. Handler is called for both key press and release events.")
)},
{NULL}
};
// Helper function to trigger lifecycle events
void McRFPy_API::triggerSceneChange(const std::string& from_scene, const std::string& to_scene)
{
// Call on_exit for the old scene
if (!from_scene.empty() && python_scenes.count(from_scene) > 0) {
PySceneClass::call_on_exit(python_scenes[from_scene]);
}
// Call on_enter for the new scene
if (!to_scene.empty() && python_scenes.count(to_scene) > 0) {
PySceneClass::call_on_enter(python_scenes[to_scene]);
}
}
// Helper function to update Python scenes
void McRFPy_API::updatePythonScenes(float dt)
{
GameEngine* game = McRFPy_API::game;
if (!game) return;
// Only update the active scene
if (python_scenes.count(game->scene) > 0) {
PySceneClass::call_update(python_scenes[game->scene], dt);
}
}
// Helper function to trigger resize events on Python scenes
void McRFPy_API::triggerResize(int width, int height)
{
GameEngine* game = McRFPy_API::game;
if (!game) return;
// Only notify the active scene
if (python_scenes.count(game->scene) > 0) {
PySceneClass::call_on_resize(python_scenes[game->scene], width, height);
}
}

View File

@ -1,63 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <string>
#include <memory>
// Forward declarations
class PyScene;
// Python object structure for Scene
typedef struct {
PyObject_HEAD
std::string name;
std::shared_ptr<PyScene> scene; // Reference to the C++ scene
bool initialized;
} PySceneObject;
// C++ interface for Python Scene class
class PySceneClass
{
public:
// Type methods
static PyObject* __new__(PyTypeObject* type, PyObject* args, PyObject* kwds);
static int __init__(PySceneObject* self, PyObject* args, PyObject* kwds);
static void __dealloc(PyObject* self);
static PyObject* __repr__(PySceneObject* self);
// Scene methods
static PyObject* activate(PySceneObject* self, PyObject* args);
static PyObject* get_ui(PySceneObject* self, PyObject* args);
static PyObject* register_keyboard(PySceneObject* self, PyObject* args);
// Properties
static PyObject* get_name(PySceneObject* self, void* closure);
static PyObject* get_active(PySceneObject* self, void* closure);
// Lifecycle callbacks (called from C++)
static void call_on_enter(PySceneObject* self);
static void call_on_exit(PySceneObject* self);
static void call_on_keypress(PySceneObject* self, std::string key, std::string action);
static void call_update(PySceneObject* self, float dt);
static void call_on_resize(PySceneObject* self, int width, int height);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
static PyTypeObject PySceneType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Scene",
.tp_basicsize = sizeof(PySceneObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PySceneClass::__dealloc,
.tp_repr = (reprfunc)PySceneClass::__repr__,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // Allow subclassing
.tp_doc = PyDoc_STR("Base class for object-oriented scenes"),
.tp_methods = nullptr, // Set in McRFPy_API.cpp
.tp_getset = nullptr, // Set in McRFPy_API.cpp
.tp_init = (initproc)PySceneClass::__init__,
.tp_new = PySceneClass::__new__,
};
}

View File

@ -1,17 +1,11 @@
#include "PyTexture.h" #include "PyTexture.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "McRFPy_Doc.h"
PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h) PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0) : source(filename), sprite_width(sprite_w), sprite_height(sprite_h)
{ {
texture = sf::Texture(); texture = sf::Texture();
if (!texture.loadFromFile(source)) { texture.loadFromFile(source);
// Failed to load texture - leave sheet dimensions as 0
// This will be checked in init()
return;
}
texture.setSmooth(false); // Disable smoothing for pixel art
auto size = texture.getSize(); auto size = texture.getSize();
sheet_width = (size.x / sprite_width); sheet_width = (size.x / sprite_width);
sheet_height = (size.y / sprite_height); sheet_height = (size.y / sprite_height);
@ -22,40 +16,8 @@ PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
} }
} }
// #144: Factory method to create texture from rendered content (snapshot)
std::shared_ptr<PyTexture> PyTexture::from_rendered(sf::RenderTexture& render_tex)
{
// Use a custom shared_ptr construction to access private default constructor
struct MakeSharedEnabler : public PyTexture {
MakeSharedEnabler() : PyTexture() {}
};
auto ptex = std::make_shared<MakeSharedEnabler>();
// Copy the rendered texture data
ptex->texture = render_tex.getTexture();
ptex->texture.setSmooth(false); // Maintain pixel art aesthetic
// Set source to indicate this is a snapshot
ptex->source = "<snapshot>";
// Treat entire texture as single sprite
auto size = ptex->texture.getSize();
ptex->sprite_width = size.x;
ptex->sprite_height = size.y;
ptex->sheet_width = 1;
ptex->sheet_height = 1;
return ptex;
}
sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s) sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s)
{ {
// Protect against division by zero if texture failed to load
if (sheet_width == 0 || sheet_height == 0) {
// Return an empty sprite
return sf::Sprite();
}
int tx = index % sheet_width, ty = index / sheet_width; int tx = index % sheet_width, ty = index / sheet_width;
auto ir = sf::IntRect(tx * sprite_width, ty * sprite_height, sprite_width, sprite_height); auto ir = sf::IntRect(tx * sprite_width, ty * sprite_height, sprite_width, sprite_height);
auto sprite = sf::Sprite(texture, ir); auto sprite = sf::Sprite(texture, ir);
@ -66,6 +28,7 @@ sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s)
PyObject* PyTexture::pyObject() PyObject* PyTexture::pyObject()
{ {
std::cout << "Find type" << std::endl;
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"); auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture");
PyObject* obj = PyTexture::pynew(type, Py_None, Py_None); PyObject* obj = PyTexture::pynew(type, Py_None, Py_None);
@ -109,16 +72,7 @@ int PyTexture::init(PyTextureObject* self, PyObject* args, PyObject* kwds)
int sprite_width, sprite_height; int sprite_width, sprite_height;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii", const_cast<char**>(keywords), &filename, &sprite_width, &sprite_height)) if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii", const_cast<char**>(keywords), &filename, &sprite_width, &sprite_height))
return -1; return -1;
// Create the texture object
self->data = std::make_shared<PyTexture>(filename, sprite_width, sprite_height); self->data = std::make_shared<PyTexture>(filename, sprite_width, sprite_height);
// Check if the texture failed to load (sheet dimensions will be 0)
if (self->data->sheet_width == 0 || self->data->sheet_height == 0) {
PyErr_Format(PyExc_IOError, "Failed to load texture from file: %s", filename);
return -1;
}
return 0; return 0;
} }
@ -126,49 +80,3 @@ PyObject* PyTexture::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
{ {
return (PyObject*)type->tp_alloc(type, 0); return (PyObject*)type->tp_alloc(type, 0);
} }
PyObject* PyTexture::get_sprite_width(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sprite_width);
}
PyObject* PyTexture::get_sprite_height(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sprite_height);
}
PyObject* PyTexture::get_sheet_width(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sheet_width);
}
PyObject* PyTexture::get_sheet_height(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->sheet_height);
}
PyObject* PyTexture::get_sprite_count(PyTextureObject* self, void* closure)
{
return PyLong_FromLong(self->data->getSpriteCount());
}
PyObject* PyTexture::get_source(PyTextureObject* self, void* closure)
{
return PyUnicode_FromString(self->data->source.c_str());
}
PyGetSetDef PyTexture::getsetters[] = {
{"sprite_width", (getter)PyTexture::get_sprite_width, NULL,
MCRF_PROPERTY(sprite_width, "Width of each sprite in pixels (int, read-only). Specified during texture initialization."), NULL},
{"sprite_height", (getter)PyTexture::get_sprite_height, NULL,
MCRF_PROPERTY(sprite_height, "Height of each sprite in pixels (int, read-only). Specified during texture initialization."), NULL},
{"sheet_width", (getter)PyTexture::get_sheet_width, NULL,
MCRF_PROPERTY(sheet_width, "Number of sprite columns in the texture sheet (int, read-only). Calculated as texture_width / sprite_width."), NULL},
{"sheet_height", (getter)PyTexture::get_sheet_height, NULL,
MCRF_PROPERTY(sheet_height, "Number of sprite rows in the texture sheet (int, read-only). Calculated as texture_height / sprite_height."), NULL},
{"sprite_count", (getter)PyTexture::get_sprite_count, NULL,
MCRF_PROPERTY(sprite_count, "Total number of sprites in the texture sheet (int, read-only). Equals sheet_width * sheet_height."), NULL},
{"source", (getter)PyTexture::get_source, NULL,
MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this texture."), NULL},
{NULL} // Sentinel
};

View File

@ -15,39 +15,20 @@ private:
sf::Texture texture; sf::Texture texture;
std::string source; std::string source;
int sheet_width, sheet_height; int sheet_width, sheet_height;
// Private default constructor for factory methods
PyTexture() : source("<uninitialized>"), sprite_width(0), sprite_height(0), sheet_width(0), sheet_height(0) {}
public: public:
int sprite_width, sprite_height; // just use them read only, OK? int sprite_width, sprite_height; // just use them read only, OK?
PyTexture(std::string filename, int sprite_w, int sprite_h); PyTexture(std::string filename, int sprite_w, int sprite_h);
// #144: Factory method to create texture from rendered content (snapshot)
static std::shared_ptr<PyTexture> from_rendered(sf::RenderTexture& render_tex);
sf::Sprite sprite(int index, sf::Vector2f pos = sf::Vector2f(0, 0), sf::Vector2f s = sf::Vector2f(1.0, 1.0)); sf::Sprite sprite(int index, sf::Vector2f pos = sf::Vector2f(0, 0), sf::Vector2f s = sf::Vector2f(1.0, 1.0));
int getSpriteCount() const { return sheet_width * sheet_height; }
PyObject* pyObject(); PyObject* pyObject();
static PyObject* repr(PyObject*); static PyObject* repr(PyObject*);
static Py_hash_t hash(PyObject*); static Py_hash_t hash(PyObject*);
static int init(PyTextureObject*, PyObject*, PyObject*); static int init(PyTextureObject*, PyObject*, PyObject*);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL); static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
// Getters for properties
static PyObject* get_sprite_width(PyTextureObject* self, void* closure);
static PyObject* get_sprite_height(PyTextureObject* self, void* closure);
static PyObject* get_sheet_width(PyTextureObject* self, void* closure);
static PyObject* get_sheet_height(PyTextureObject* self, void* closure);
static PyObject* get_sprite_count(PyTextureObject* self, void* closure);
static PyObject* get_source(PyTextureObject* self, void* closure);
static PyGetSetDef getsetters[];
}; };
namespace mcrfpydef { namespace mcrfpydef {
static PyTypeObject PyTextureType = { static PyTypeObject PyTextureType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Texture", .tp_name = "mcrfpy.Texture",
.tp_basicsize = sizeof(PyTextureObject), .tp_basicsize = sizeof(PyTextureObject),
.tp_itemsize = 0, .tp_itemsize = 0,
@ -55,7 +36,6 @@ namespace mcrfpydef {
.tp_hash = PyTexture::hash, .tp_hash = PyTexture::hash,
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Texture Object"), .tp_doc = PyDoc_STR("SFML Texture Object"),
.tp_getset = PyTexture::getsetters,
//.tp_base = &PyBaseObject_Type, //.tp_base = &PyBaseObject_Type,
.tp_init = (initproc)PyTexture::init, .tp_init = (initproc)PyTexture::init,
.tp_new = PyType_GenericNew, //PyTexture::pynew, .tp_new = PyType_GenericNew, //PyTexture::pynew,

View File

@ -1,357 +0,0 @@
#include "PyTimer.h"
#include "Timer.h"
#include "GameEngine.h"
#include "Resources.h"
#include "PythonObjectCache.h"
#include "McRFPy_Doc.h"
#include <sstream>
PyObject* PyTimer::repr(PyObject* self) {
PyTimerObject* timer = (PyTimerObject*)self;
std::ostringstream oss;
oss << "<Timer name='" << timer->name << "' ";
if (timer->data) {
oss << "interval=" << timer->data->getInterval() << "ms ";
if (timer->data->isOnce()) {
oss << "once=True ";
}
if (timer->data->isPaused()) {
oss << "paused";
// Get current time to show remaining
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
oss << " (remaining=" << timer->data->getRemaining(current_time) << "ms)";
} else if (timer->data->isActive()) {
oss << "active";
} else {
oss << "cancelled";
}
} else {
oss << "uninitialized";
}
oss << ">";
return PyUnicode_FromString(oss.str().c_str());
}
PyObject* PyTimer::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyTimerObject* self = (PyTimerObject*)type->tp_alloc(type, 0);
if (self) {
new(&self->name) std::string(); // Placement new for std::string
self->data = nullptr;
self->weakreflist = nullptr; // Initialize weakref list
}
return (PyObject*)self;
}
int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"name", "callback", "interval", "once", NULL};
const char* name = nullptr;
PyObject* callback = nullptr;
int interval = 0;
int once = 0; // Use int for bool parameter
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi|p", const_cast<char**>(kwlist),
&name, &callback, &interval, &once)) {
return -1;
}
if (!PyCallable_Check(callback)) {
PyErr_SetString(PyExc_TypeError, "callback must be callable");
return -1;
}
if (interval <= 0) {
PyErr_SetString(PyExc_ValueError, "interval must be positive");
return -1;
}
self->name = name;
// Get current time from game engine
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
// Create the timer
self->data = std::make_shared<Timer>(callback, interval, current_time, (bool)once);
// Register in Python object cache
if (self->data->serial_number == 0) {
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
Py_DECREF(weakref); // Cache owns the reference now
}
}
// Register with game engine
if (Resources::game) {
Resources::game->timers[self->name] = self->data;
}
return 0;
}
void PyTimer::dealloc(PyTimerObject* self) {
// Clear weakrefs first
if (self->weakreflist != nullptr) {
PyObject_ClearWeakRefs((PyObject*)self);
}
// Remove from game engine if still registered
if (Resources::game && !self->name.empty()) {
auto it = Resources::game->timers.find(self->name);
if (it != Resources::game->timers.end() && it->second == self->data) {
Resources::game->timers.erase(it);
}
}
// Explicitly destroy std::string
self->name.~basic_string();
// Clear shared_ptr
self->data.reset();
Py_TYPE(self)->tp_free((PyObject*)self);
}
// Timer control methods
PyObject* PyTimer::pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
self->data->pause(current_time);
Py_RETURN_NONE;
}
PyObject* PyTimer::resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
self->data->resume(current_time);
Py_RETURN_NONE;
}
PyObject* PyTimer::cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
// Remove from game engine
if (Resources::game && !self->name.empty()) {
auto it = Resources::game->timers.find(self->name);
if (it != Resources::game->timers.end() && it->second == self->data) {
Resources::game->timers.erase(it);
}
}
self->data->cancel();
self->data.reset();
Py_RETURN_NONE;
}
PyObject* PyTimer::restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
self->data->restart(current_time);
Py_RETURN_NONE;
}
// Property getters/setters
PyObject* PyTimer::get_interval(PyTimerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
return PyLong_FromLong(self->data->getInterval());
}
int PyTimer::set_interval(PyTimerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return -1;
}
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "interval must be an integer");
return -1;
}
long interval = PyLong_AsLong(value);
if (interval <= 0) {
PyErr_SetString(PyExc_ValueError, "interval must be positive");
return -1;
}
self->data->setInterval(interval);
return 0;
}
PyObject* PyTimer::get_remaining(PyTimerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
return PyLong_FromLong(self->data->getRemaining(current_time));
}
PyObject* PyTimer::get_paused(PyTimerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
return PyBool_FromLong(self->data->isPaused());
}
PyObject* PyTimer::get_active(PyTimerObject* self, void* closure) {
if (!self->data) {
return Py_False;
}
return PyBool_FromLong(self->data->isActive());
}
PyObject* PyTimer::get_callback(PyTimerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
PyObject* callback = self->data->getCallback();
if (!callback) {
Py_RETURN_NONE;
}
Py_INCREF(callback);
return callback;
}
int PyTimer::set_callback(PyTimerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return -1;
}
if (!PyCallable_Check(value)) {
PyErr_SetString(PyExc_TypeError, "callback must be callable");
return -1;
}
self->data->setCallback(value);
return 0;
}
PyObject* PyTimer::get_once(PyTimerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
return PyBool_FromLong(self->data->isOnce());
}
int PyTimer::set_once(PyTimerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return -1;
}
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "once must be a boolean");
return -1;
}
self->data->setOnce(PyObject_IsTrue(value));
return 0;
}
PyObject* PyTimer::get_name(PyTimerObject* self, void* closure) {
return PyUnicode_FromString(self->name.c_str());
}
PyGetSetDef PyTimer::getsetters[] = {
{"name", (getter)PyTimer::get_name, NULL,
MCRF_PROPERTY(name, "Timer name (str, read-only). Unique identifier for this timer."), NULL},
{"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval,
MCRF_PROPERTY(interval, "Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running."), NULL},
{"remaining", (getter)PyTimer::get_remaining, NULL,
MCRF_PROPERTY(remaining, "Time remaining until next trigger in milliseconds (int, read-only). Preserved when timer is paused."), NULL},
{"paused", (getter)PyTimer::get_paused, NULL,
MCRF_PROPERTY(paused, "Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time."), NULL},
{"active", (getter)PyTimer::get_active, NULL,
MCRF_PROPERTY(active, "Whether the timer is active and not paused (bool, read-only). False if cancelled or paused."), NULL},
{"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback,
MCRF_PROPERTY(callback, "The callback function to be called when timer fires (callable). Can be changed while timer is running."), NULL},
{"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once,
MCRF_PROPERTY(once, "Whether the timer stops after firing once (bool). If False, timer repeats indefinitely."), NULL},
{NULL}
};
PyMethodDef PyTimer::methods[] = {
{"pause", (PyCFunction)PyTimer::pause, METH_NOARGS,
MCRF_METHOD(Timer, pause,
MCRF_SIG("()", "None"),
MCRF_DESC("Pause the timer, preserving the time remaining until next trigger."),
MCRF_RETURNS("None")
MCRF_NOTE("The timer can be resumed later with resume(). Time spent paused does not count toward the interval.")
)},
{"resume", (PyCFunction)PyTimer::resume, METH_NOARGS,
MCRF_METHOD(Timer, resume,
MCRF_SIG("()", "None"),
MCRF_DESC("Resume a paused timer from where it left off."),
MCRF_RETURNS("None")
MCRF_NOTE("Has no effect if the timer is not paused. Timer will fire after the remaining time elapses.")
)},
{"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS,
MCRF_METHOD(Timer, cancel,
MCRF_SIG("()", "None"),
MCRF_DESC("Cancel the timer and remove it from the timer system."),
MCRF_RETURNS("None")
MCRF_NOTE("The timer will no longer fire and cannot be restarted. The callback will not be called again.")
)},
{"restart", (PyCFunction)PyTimer::restart, METH_NOARGS,
MCRF_METHOD(Timer, restart,
MCRF_SIG("()", "None"),
MCRF_DESC("Restart the timer from the beginning."),
MCRF_RETURNS("None")
MCRF_NOTE("Resets the timer to fire after a full interval from now, regardless of remaining time.")
)},
{NULL}
};

View File

@ -1,90 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <memory>
#include <string>
class Timer;
typedef struct {
PyObject_HEAD
std::shared_ptr<Timer> data;
std::string name;
PyObject* weakreflist; // Weak reference support
} PyTimerObject;
class PyTimer
{
public:
// Python type methods
static PyObject* repr(PyObject* self);
static int init(PyTimerObject* self, PyObject* args, PyObject* kwds);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
static void dealloc(PyTimerObject* self);
// Timer control methods
static PyObject* pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
// Timer property getters
static PyObject* get_name(PyTimerObject* self, void* closure);
static PyObject* get_interval(PyTimerObject* self, void* closure);
static int set_interval(PyTimerObject* self, PyObject* value, void* closure);
static PyObject* get_remaining(PyTimerObject* self, void* closure);
static PyObject* get_paused(PyTimerObject* self, void* closure);
static PyObject* get_active(PyTimerObject* self, void* closure);
static PyObject* get_callback(PyTimerObject* self, void* closure);
static int set_callback(PyTimerObject* self, PyObject* value, void* closure);
static PyObject* get_once(PyTimerObject* self, void* closure);
static int set_once(PyTimerObject* self, PyObject* value, void* closure);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
static PyTypeObject PyTimerType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Timer",
.tp_basicsize = sizeof(PyTimerObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyTimer::dealloc,
.tp_repr = PyTimer::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Timer(name, callback, interval, once=False)\n\n"
"Create a timer that calls a function at regular intervals.\n\n"
"Args:\n"
" name (str): Unique identifier for the timer\n"
" callback (callable): Function to call - receives (timer, runtime) args\n"
" interval (int): Time between calls in milliseconds\n"
" once (bool): If True, timer stops after first call. Default: False\n\n"
"Attributes:\n"
" interval (int): Time between calls in milliseconds\n"
" remaining (int): Time until next call in milliseconds (read-only)\n"
" paused (bool): Whether timer is paused (read-only)\n"
" active (bool): Whether timer is active and not paused (read-only)\n"
" callback (callable): The callback function\n"
" once (bool): Whether timer stops after firing once\n\n"
"Methods:\n"
" pause(): Pause the timer, preserving time remaining\n"
" resume(): Resume a paused timer\n"
" cancel(): Stop and remove the timer\n"
" restart(): Reset timer to start from beginning\n\n"
"Example:\n"
" def on_timer(timer, runtime):\n"
" print(f'Timer {timer} fired at {runtime}ms')\n"
" if runtime > 5000:\n"
" timer.cancel()\n"
" \n"
" timer = mcrfpy.Timer('my_timer', on_timer, 1000)\n"
" timer.pause() # Pause timer\n"
" timer.resume() # Resume timer\n"
" timer.once = True # Make it one-shot"),
.tp_methods = PyTimer::methods,
.tp_getset = PyTimer::getsetters,
.tp_init = (initproc)PyTimer::init,
.tp_new = PyTimer::pynew,
};
}

View File

@ -1,147 +1,21 @@
#include "PyVector.h" #include "PyVector.h"
#include "PyObjectUtils.h"
#include "McRFPy_Doc.h"
#include "PyRAII.h"
#include <cmath>
PyGetSetDef PyVector::getsetters[] = { PyGetSetDef PyVector::getsetters[] = {
{"x", (getter)PyVector::get_member, (setter)PyVector::set_member, {"x", (getter)PyVector::get_member, (setter)PyVector::set_member, "X/horizontal component", (void*)0},
MCRF_PROPERTY(x, "X coordinate of the vector (float)"), (void*)0}, {"y", (getter)PyVector::get_member, (setter)PyVector::set_member, "Y/vertical component", (void*)1},
{"y", (getter)PyVector::get_member, (setter)PyVector::set_member,
MCRF_PROPERTY(y, "Y coordinate of the vector (float)"), (void*)1},
{"int", (getter)PyVector::get_int, NULL,
MCRF_PROPERTY(int, "Integer tuple (floor of x and y) for use as dict keys. Read-only."), NULL},
{NULL} {NULL}
}; };
PyMethodDef PyVector::methods[] = {
{"magnitude", (PyCFunction)PyVector::magnitude, METH_NOARGS,
MCRF_METHOD(Vector, magnitude,
MCRF_SIG("()", "float"),
MCRF_DESC("Calculate the length/magnitude of this vector."),
MCRF_RETURNS("float: The magnitude of the vector")
)},
{"magnitude_squared", (PyCFunction)PyVector::magnitude_squared, METH_NOARGS,
MCRF_METHOD(Vector, magnitude_squared,
MCRF_SIG("()", "float"),
MCRF_DESC("Calculate the squared magnitude of this vector."),
MCRF_RETURNS("float: The squared magnitude (faster than magnitude())")
MCRF_NOTE("Use this for comparisons to avoid expensive square root calculation.")
)},
{"normalize", (PyCFunction)PyVector::normalize, METH_NOARGS,
MCRF_METHOD(Vector, normalize,
MCRF_SIG("()", "Vector"),
MCRF_DESC("Return a unit vector in the same direction."),
MCRF_RETURNS("Vector: New normalized vector with magnitude 1.0")
MCRF_NOTE("For zero vectors (magnitude 0.0), returns a zero vector rather than raising an exception")
)},
{"dot", (PyCFunction)PyVector::dot, METH_O,
MCRF_METHOD(Vector, dot,
MCRF_SIG("(other: Vector)", "float"),
MCRF_DESC("Calculate the dot product with another vector."),
MCRF_ARGS_START
MCRF_ARG("other", "The other vector")
MCRF_RETURNS("float: Dot product of the two vectors")
)},
{"distance_to", (PyCFunction)PyVector::distance_to, METH_O,
MCRF_METHOD(Vector, distance_to,
MCRF_SIG("(other: Vector)", "float"),
MCRF_DESC("Calculate the distance to another vector."),
MCRF_ARGS_START
MCRF_ARG("other", "The other vector")
MCRF_RETURNS("float: Distance between the two vectors")
)},
{"angle", (PyCFunction)PyVector::angle, METH_NOARGS,
MCRF_METHOD(Vector, angle,
MCRF_SIG("()", "float"),
MCRF_DESC("Get the angle of this vector in radians."),
MCRF_RETURNS("float: Angle in radians from positive x-axis")
)},
{"copy", (PyCFunction)PyVector::copy, METH_NOARGS,
MCRF_METHOD(Vector, copy,
MCRF_SIG("()", "Vector"),
MCRF_DESC("Create a copy of this vector."),
MCRF_RETURNS("Vector: New Vector object with same x and y values")
)},
{"floor", (PyCFunction)PyVector::floor, METH_NOARGS,
MCRF_METHOD(Vector, floor,
MCRF_SIG("()", "Vector"),
MCRF_DESC("Return a new vector with floored (integer) coordinates."),
MCRF_RETURNS("Vector: New Vector with floor(x) and floor(y)")
MCRF_NOTE("Useful for grid-based positioning. For a hashable tuple, use the .int property instead.")
)},
{NULL}
};
namespace mcrfpydef {
PyNumberMethods PyVector_as_number = {
.nb_add = PyVector::add,
.nb_subtract = PyVector::subtract,
.nb_multiply = PyVector::multiply,
.nb_remainder = 0,
.nb_divmod = 0,
.nb_power = 0,
.nb_negative = PyVector::negative,
.nb_positive = 0,
.nb_absolute = PyVector::absolute,
.nb_bool = PyVector::bool_check,
.nb_invert = 0,
.nb_lshift = 0,
.nb_rshift = 0,
.nb_and = 0,
.nb_xor = 0,
.nb_or = 0,
.nb_int = 0,
.nb_reserved = 0,
.nb_float = 0,
.nb_inplace_add = 0,
.nb_inplace_subtract = 0,
.nb_inplace_multiply = 0,
.nb_inplace_remainder = 0,
.nb_inplace_power = 0,
.nb_inplace_lshift = 0,
.nb_inplace_rshift = 0,
.nb_inplace_and = 0,
.nb_inplace_xor = 0,
.nb_inplace_or = 0,
.nb_floor_divide = 0,
.nb_true_divide = PyVector::divide,
.nb_inplace_floor_divide = 0,
.nb_inplace_true_divide = 0,
.nb_index = 0,
.nb_matrix_multiply = 0,
.nb_inplace_matrix_multiply = 0
};
PySequenceMethods PyVector_as_sequence = {
.sq_length = PyVector::sequence_length,
.sq_concat = 0,
.sq_repeat = 0,
.sq_item = PyVector::sequence_item,
.was_sq_slice = 0,
.sq_ass_item = 0,
.was_sq_ass_slice = 0,
.sq_contains = 0,
.sq_inplace_concat = 0,
.sq_inplace_repeat = 0
};
}
PyVector::PyVector(sf::Vector2f target) PyVector::PyVector(sf::Vector2f target)
:data(target) {} :data(target) {}
PyObject* PyVector::pyObject() PyObject* PyVector::pyObject()
{ {
PyTypeObject* type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); PyObject* obj = PyType_GenericAlloc(&mcrfpydef::PyVectorType, 0);
if (!type) return nullptr; Py_INCREF(obj);
PyVectorObject* self = (PyVectorObject*)obj;
PyVectorObject* obj = (PyVectorObject*)type->tp_alloc(type, 0); self->data = data;
Py_DECREF(type); return obj;
if (obj) {
obj->data = data;
}
return (PyObject*)obj;
} }
sf::Vector2f PyVector::fromPy(PyObject* obj) sf::Vector2f PyVector::fromPy(PyObject* obj)
@ -226,398 +100,12 @@ PyObject* PyVector::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
PyObject* PyVector::get_member(PyObject* obj, void* closure) PyObject* PyVector::get_member(PyObject* obj, void* closure)
{ {
PyVectorObject* self = (PyVectorObject*)obj; // TODO
if (reinterpret_cast<long>(closure) == 0) { return Py_None;
// x
return PyFloat_FromDouble(self->data.x);
} else {
// y
return PyFloat_FromDouble(self->data.y);
}
} }
int PyVector::set_member(PyObject* obj, PyObject* value, void* closure) int PyVector::set_member(PyObject* obj, PyObject* value, void* closure)
{ {
PyVectorObject* self = (PyVectorObject*)obj; // TODO
float val;
if (PyFloat_Check(value)) {
val = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
val = PyLong_AsDouble(value);
} else {
PyErr_SetString(PyExc_TypeError, "Vector members must be numeric");
return -1;
}
if (reinterpret_cast<long>(closure) == 0) {
// x
self->data.x = val;
} else {
// y
self->data.y = val;
}
return 0; return 0;
} }
PyVectorObject* PyVector::from_arg(PyObject* args)
{
// Use RAII for type reference management
PyRAII::PyTypeRef type("Vector", McRFPy_API::mcrf_module);
if (!type) {
return NULL;
}
// Check if args is already a Vector instance
if (PyObject_IsInstance(args, (PyObject*)type.get())) {
Py_INCREF(args); // Return new reference so caller can safely DECREF
return (PyVectorObject*)args;
}
// Create new Vector object using RAII
PyRAII::PyObjectRef obj(type->tp_alloc(type.get(), 0), true);
if (!obj) {
return NULL;
}
// Handle different input types
if (PyTuple_Check(args)) {
// It's already a tuple, pass it directly to init
int err = init((PyVectorObject*)obj.get(), args, NULL);
if (err) {
// obj will be automatically cleaned up when it goes out of scope
return NULL;
}
} else {
// Wrap single argument in a tuple for init
PyRAII::PyObjectRef tuple(PyTuple_Pack(1, args), true);
if (!tuple) {
return NULL;
}
int err = init((PyVectorObject*)obj.get(), tuple.get(), NULL);
if (err) {
return NULL;
}
}
// Release ownership and return
return (PyVectorObject*)obj.release();
}
// Arithmetic operations
PyObject* PyVector::add(PyObject* left, PyObject* right)
{
// Check if both operands are vectors
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
PyVectorObject* vec1 = nullptr;
PyVectorObject* vec2 = nullptr;
if (PyObject_IsInstance(left, (PyObject*)type) && PyObject_IsInstance(right, (PyObject*)type)) {
vec1 = (PyVectorObject*)left;
vec2 = (PyVectorObject*)right;
} else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(vec1->data.x + vec2->data.x, vec1->data.y + vec2->data.y);
}
return (PyObject*)result;
}
PyObject* PyVector::subtract(PyObject* left, PyObject* right)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
PyVectorObject* vec1 = nullptr;
PyVectorObject* vec2 = nullptr;
if (PyObject_IsInstance(left, (PyObject*)type) && PyObject_IsInstance(right, (PyObject*)type)) {
vec1 = (PyVectorObject*)left;
vec2 = (PyVectorObject*)right;
} else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(vec1->data.x - vec2->data.x, vec1->data.y - vec2->data.y);
}
return (PyObject*)result;
}
PyObject* PyVector::multiply(PyObject* left, PyObject* right)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
PyVectorObject* vec = nullptr;
double scalar = 0.0;
// Check for Vector * scalar
if (PyObject_IsInstance(left, (PyObject*)type) && (PyFloat_Check(right) || PyLong_Check(right))) {
vec = (PyVectorObject*)left;
scalar = PyFloat_AsDouble(right);
}
// Check for scalar * Vector
else if ((PyFloat_Check(left) || PyLong_Check(left)) && PyObject_IsInstance(right, (PyObject*)type)) {
scalar = PyFloat_AsDouble(left);
vec = (PyVectorObject*)right;
}
else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(vec->data.x * scalar, vec->data.y * scalar);
}
return (PyObject*)result;
}
PyObject* PyVector::divide(PyObject* left, PyObject* right)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
// Only support Vector / scalar
if (!PyObject_IsInstance(left, (PyObject*)type) || (!PyFloat_Check(right) && !PyLong_Check(right))) {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
PyVectorObject* vec = (PyVectorObject*)left;
double scalar = PyFloat_AsDouble(right);
if (scalar == 0.0) {
PyErr_SetString(PyExc_ZeroDivisionError, "Vector division by zero");
return NULL;
}
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(vec->data.x / scalar, vec->data.y / scalar);
}
return (PyObject*)result;
}
PyObject* PyVector::negative(PyObject* self)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
PyVectorObject* vec = (PyVectorObject*)self;
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(-vec->data.x, -vec->data.y);
}
return (PyObject*)result;
}
PyObject* PyVector::absolute(PyObject* self)
{
PyVectorObject* vec = (PyVectorObject*)self;
return PyFloat_FromDouble(std::sqrt(vec->data.x * vec->data.x + vec->data.y * vec->data.y));
}
int PyVector::bool_check(PyObject* self)
{
PyVectorObject* vec = (PyVectorObject*)self;
return (vec->data.x != 0.0f || vec->data.y != 0.0f) ? 1 : 0;
}
PyObject* PyVector::richcompare(PyObject* left, PyObject* right, int op)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
float left_x, left_y, right_x, right_y;
// Extract left operand values
if (PyObject_IsInstance(left, (PyObject*)type)) {
PyVectorObject* vec = (PyVectorObject*)left;
left_x = vec->data.x;
left_y = vec->data.y;
} else if (PyTuple_Check(left) && PyTuple_Size(left) == 2) {
PyObject* x_obj = PyTuple_GetItem(left, 0);
PyObject* y_obj = PyTuple_GetItem(left, 1);
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
left_x = (float)PyFloat_AsDouble(x_obj);
left_y = (float)PyFloat_AsDouble(y_obj);
} else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
} else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
// Extract right operand values
if (PyObject_IsInstance(right, (PyObject*)type)) {
PyVectorObject* vec = (PyVectorObject*)right;
right_x = vec->data.x;
right_y = vec->data.y;
} else if (PyTuple_Check(right) && PyTuple_Size(right) == 2) {
PyObject* x_obj = PyTuple_GetItem(right, 0);
PyObject* y_obj = PyTuple_GetItem(right, 1);
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
right_x = (float)PyFloat_AsDouble(x_obj);
right_y = (float)PyFloat_AsDouble(y_obj);
} else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
} else {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
bool result = false;
switch (op) {
case Py_EQ:
result = (left_x == right_x && left_y == right_y);
break;
case Py_NE:
result = (left_x != right_x || left_y != right_y);
break;
default:
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}
if (result)
Py_RETURN_TRUE;
else
Py_RETURN_FALSE;
}
// Vector-specific methods
PyObject* PyVector::magnitude(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
float mag = std::sqrt(self->data.x * self->data.x + self->data.y * self->data.y);
return PyFloat_FromDouble(mag);
}
PyObject* PyVector::magnitude_squared(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
float mag_sq = self->data.x * self->data.x + self->data.y * self->data.y;
return PyFloat_FromDouble(mag_sq);
}
PyObject* PyVector::normalize(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
float mag = std::sqrt(self->data.x * self->data.x + self->data.y * self->data.y);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
if (mag > 0.0f) {
result->data = sf::Vector2f(self->data.x / mag, self->data.y / mag);
} else {
// Zero vector remains zero
result->data = sf::Vector2f(0.0f, 0.0f);
}
}
return (PyObject*)result;
}
PyObject* PyVector::dot(PyVectorObject* self, PyObject* other)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (!PyObject_IsInstance(other, (PyObject*)type)) {
PyErr_SetString(PyExc_TypeError, "Argument must be a Vector");
return NULL;
}
PyVectorObject* vec2 = (PyVectorObject*)other;
float dot_product = self->data.x * vec2->data.x + self->data.y * vec2->data.y;
return PyFloat_FromDouble(dot_product);
}
PyObject* PyVector::distance_to(PyVectorObject* self, PyObject* other)
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (!PyObject_IsInstance(other, (PyObject*)type)) {
PyErr_SetString(PyExc_TypeError, "Argument must be a Vector");
return NULL;
}
PyVectorObject* vec2 = (PyVectorObject*)other;
float dx = self->data.x - vec2->data.x;
float dy = self->data.y - vec2->data.y;
float distance = std::sqrt(dx * dx + dy * dy);
return PyFloat_FromDouble(distance);
}
PyObject* PyVector::angle(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
float angle_rad = std::atan2(self->data.y, self->data.x);
return PyFloat_FromDouble(angle_rad);
}
PyObject* PyVector::copy(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = self->data;
}
return (PyObject*)result;
}
PyObject* PyVector::floor(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
if (result) {
result->data = sf::Vector2f(std::floor(self->data.x), std::floor(self->data.y));
}
return (PyObject*)result;
}
// Sequence protocol implementation
Py_ssize_t PyVector::sequence_length(PyObject* self)
{
return 2; // Vectors always have exactly 2 elements
}
PyObject* PyVector::sequence_item(PyObject* obj, Py_ssize_t index)
{
PyVectorObject* self = (PyVectorObject*)obj;
// Note: Python already handles negative index normalization when sq_length is defined
// So v[-1] arrives here as index=1, v[-2] as index=0
// Out-of-range negative indices (like v[-3]) arrive as negative values (e.g., -1)
if (index == 0) {
return PyFloat_FromDouble(self->data.x);
} else if (index == 1) {
return PyFloat_FromDouble(self->data.y);
} else {
PyErr_SetString(PyExc_IndexError, "Vector index out of range (must be 0 or 1)");
return NULL;
}
}
// Property: .int - returns integer tuple for use as dict keys
PyObject* PyVector::get_int(PyObject* obj, void* closure)
{
PyVectorObject* self = (PyVectorObject*)obj;
long ix = (long)std::floor(self->data.x);
long iy = (long)std::floor(self->data.y);
return Py_BuildValue("(ll)", ix, iy);
}

View File

@ -1,7 +1,6 @@
#pragma once #pragma once
#include "Common.h" #include "Common.h"
#include "Python.h" #include "Python.h"
#include "McRFPy_API.h"
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
@ -23,59 +22,19 @@ public:
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL); static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
static PyObject* get_member(PyObject*, void*); static PyObject* get_member(PyObject*, void*);
static int set_member(PyObject*, PyObject*, void*); static int set_member(PyObject*, PyObject*, void*);
static PyVectorObject* from_arg(PyObject*);
// Arithmetic operations
static PyObject* add(PyObject*, PyObject*);
static PyObject* subtract(PyObject*, PyObject*);
static PyObject* multiply(PyObject*, PyObject*);
static PyObject* divide(PyObject*, PyObject*);
static PyObject* negative(PyObject*);
static PyObject* absolute(PyObject*);
static int bool_check(PyObject*);
// Comparison operations
static PyObject* richcompare(PyObject*, PyObject*, int);
// Vector operations
static PyObject* magnitude(PyVectorObject*, PyObject*);
static PyObject* magnitude_squared(PyVectorObject*, PyObject*);
static PyObject* normalize(PyVectorObject*, PyObject*);
static PyObject* dot(PyVectorObject*, PyObject*);
static PyObject* distance_to(PyVectorObject*, PyObject*);
static PyObject* angle(PyVectorObject*, PyObject*);
static PyObject* copy(PyVectorObject*, PyObject*);
static PyObject* floor(PyVectorObject*, PyObject*);
// Sequence protocol
static Py_ssize_t sequence_length(PyObject*);
static PyObject* sequence_item(PyObject*, Py_ssize_t);
// Additional properties
static PyObject* get_int(PyObject*, void*);
static PyGetSetDef getsetters[]; static PyGetSetDef getsetters[];
static PyMethodDef methods[];
}; };
namespace mcrfpydef { namespace mcrfpydef {
// Forward declare the PyNumberMethods and PySequenceMethods structures
extern PyNumberMethods PyVector_as_number;
extern PySequenceMethods PyVector_as_sequence;
static PyTypeObject PyVectorType = { static PyTypeObject PyVectorType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Vector", .tp_name = "mcrfpy.Vector",
.tp_basicsize = sizeof(PyVectorObject), .tp_basicsize = sizeof(PyVectorObject),
.tp_itemsize = 0, .tp_itemsize = 0,
.tp_repr = PyVector::repr, .tp_repr = PyVector::repr,
.tp_as_number = &PyVector_as_number,
.tp_as_sequence = &PyVector_as_sequence,
.tp_hash = PyVector::hash, .tp_hash = PyVector::hash,
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Vector Object"), .tp_doc = PyDoc_STR("SFML Vector Object"),
.tp_richcompare = PyVector::richcompare,
.tp_methods = PyVector::methods,
.tp_getset = PyVector::getsetters, .tp_getset = PyVector::getsetters,
.tp_init = (initproc)PyVector::init, .tp_init = (initproc)PyVector::init,
.tp_new = PyVector::pynew, .tp_new = PyVector::pynew,

View File

@ -1,532 +0,0 @@
#include "PyWindow.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include <SFML/Graphics.hpp>
#include <cstring>
// Singleton instance - static variable, not a class member
static PyWindowObject* window_instance = nullptr;
PyObject* PyWindow::get(PyObject* cls, PyObject* args)
{
// Create singleton instance if it doesn't exist
if (!window_instance) {
// Use the class object passed as first argument
PyTypeObject* type = (PyTypeObject*)cls;
if (!type->tp_alloc) {
PyErr_SetString(PyExc_RuntimeError, "Window type not properly initialized");
return NULL;
}
window_instance = (PyWindowObject*)type->tp_alloc(type, 0);
if (!window_instance) {
return NULL;
}
}
Py_INCREF(window_instance);
return (PyObject*)window_instance;
}
PyObject* PyWindow::repr(PyWindowObject* self)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
return PyUnicode_FromString("<Window [no game engine]>");
}
if (game->isHeadless()) {
return PyUnicode_FromString("<Window [headless mode]>");
}
auto& window = game->getWindow();
auto size = window.getSize();
return PyUnicode_FromFormat("<Window %dx%d>", size.x, size.y);
}
// Property getters and setters
PyObject* PyWindow::get_resolution(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
if (game->isHeadless()) {
// Return headless renderer size
return Py_BuildValue("(ii)", 1024, 768); // Default headless size
}
auto& window = game->getWindow();
auto size = window.getSize();
return Py_BuildValue("(ii)", size.x, size.y);
}
int PyWindow::set_resolution(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
PyErr_SetString(PyExc_RuntimeError, "Cannot change resolution in headless mode");
return -1;
}
int width, height;
if (!PyArg_ParseTuple(value, "ii", &width, &height)) {
PyErr_SetString(PyExc_TypeError, "Resolution must be a tuple of two integers (width, height)");
return -1;
}
if (width <= 0 || height <= 0) {
PyErr_SetString(PyExc_ValueError, "Resolution dimensions must be positive");
return -1;
}
auto& window = game->getWindow();
// Get current window settings
auto style = sf::Style::Titlebar | sf::Style::Close;
if (window.getSize() == sf::Vector2u(sf::VideoMode::getDesktopMode().width,
sf::VideoMode::getDesktopMode().height)) {
style = sf::Style::Fullscreen;
}
// Recreate window with new size
window.create(sf::VideoMode(width, height), game->getWindowTitle(), style);
// Restore vsync and framerate settings
// Note: We'll need to store these settings in GameEngine
window.setFramerateLimit(60); // Default for now
return 0;
}
PyObject* PyWindow::get_fullscreen(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
if (game->isHeadless()) {
Py_RETURN_FALSE;
}
auto& window = game->getWindow();
auto size = window.getSize();
auto desktop = sf::VideoMode::getDesktopMode();
// Check if window size matches desktop size (rough fullscreen check)
bool fullscreen = (size.x == desktop.width && size.y == desktop.height);
return PyBool_FromLong(fullscreen);
}
int PyWindow::set_fullscreen(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
PyErr_SetString(PyExc_RuntimeError, "Cannot change fullscreen in headless mode");
return -1;
}
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "Fullscreen must be a boolean");
return -1;
}
bool fullscreen = PyObject_IsTrue(value);
auto& window = game->getWindow();
if (fullscreen) {
// Switch to fullscreen
auto desktop = sf::VideoMode::getDesktopMode();
window.create(desktop, game->getWindowTitle(), sf::Style::Fullscreen);
} else {
// Switch to windowed mode
window.create(sf::VideoMode(1024, 768), game->getWindowTitle(),
sf::Style::Titlebar | sf::Style::Close);
}
// Restore settings
window.setFramerateLimit(60);
return 0;
}
PyObject* PyWindow::get_vsync(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
return PyBool_FromLong(game->getVSync());
}
int PyWindow::set_vsync(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
PyErr_SetString(PyExc_RuntimeError, "Cannot change vsync in headless mode");
return -1;
}
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "vsync must be a boolean");
return -1;
}
bool vsync = PyObject_IsTrue(value);
game->setVSync(vsync);
return 0;
}
PyObject* PyWindow::get_title(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
return PyUnicode_FromString(game->getWindowTitle().c_str());
}
int PyWindow::set_title(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
// Silently ignore in headless mode
return 0;
}
const char* title = PyUnicode_AsUTF8(value);
if (!title) {
PyErr_SetString(PyExc_TypeError, "Title must be a string");
return -1;
}
game->setWindowTitle(title);
return 0;
}
PyObject* PyWindow::get_visible(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
if (game->isHeadless()) {
Py_RETURN_FALSE;
}
auto& window = game->getWindow();
bool visible = window.isOpen(); // Best approximation
return PyBool_FromLong(visible);
}
int PyWindow::set_visible(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
// Silently ignore in headless mode
return 0;
}
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
bool visible = PyObject_IsTrue(value);
auto& window = game->getWindow();
window.setVisible(visible);
return 0;
}
PyObject* PyWindow::get_framerate_limit(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
return PyLong_FromLong(game->getFramerateLimit());
}
int PyWindow::set_framerate_limit(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
if (game->isHeadless()) {
// Silently ignore in headless mode
return 0;
}
long limit = PyLong_AsLong(value);
if (PyErr_Occurred()) {
PyErr_SetString(PyExc_TypeError, "framerate_limit must be an integer");
return -1;
}
if (limit < 0) {
PyErr_SetString(PyExc_ValueError, "framerate_limit must be non-negative (0 for unlimited)");
return -1;
}
game->setFramerateLimit(limit);
return 0;
}
// Methods
PyObject* PyWindow::center(PyWindowObject* self, PyObject* args)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
if (game->isHeadless()) {
PyErr_SetString(PyExc_RuntimeError, "Cannot center window in headless mode");
return NULL;
}
auto& window = game->getWindow();
auto size = window.getSize();
auto desktop = sf::VideoMode::getDesktopMode();
int x = (desktop.width - size.x) / 2;
int y = (desktop.height - size.y) / 2;
window.setPosition(sf::Vector2i(x, y));
Py_RETURN_NONE;
}
PyObject* PyWindow::screenshot(PyWindowObject* self, PyObject* args, PyObject* kwds)
{
static const char* keywords[] = {"filename", NULL};
const char* filename = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|s", const_cast<char**>(keywords), &filename)) {
return NULL;
}
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
// Get the render target pointer
sf::RenderTarget* target = game->getRenderTargetPtr();
if (!target) {
PyErr_SetString(PyExc_RuntimeError, "No render target available");
return NULL;
}
sf::Image screenshot;
// For RenderWindow
if (auto* window = dynamic_cast<sf::RenderWindow*>(target)) {
sf::Vector2u windowSize = window->getSize();
sf::Texture texture;
texture.create(windowSize.x, windowSize.y);
texture.update(*window);
screenshot = texture.copyToImage();
}
// For RenderTexture (headless mode)
else if (auto* renderTexture = dynamic_cast<sf::RenderTexture*>(target)) {
screenshot = renderTexture->getTexture().copyToImage();
}
else {
PyErr_SetString(PyExc_RuntimeError, "Unknown render target type");
return NULL;
}
// Save to file if filename provided
if (filename) {
if (!screenshot.saveToFile(filename)) {
PyErr_SetString(PyExc_IOError, "Failed to save screenshot");
return NULL;
}
Py_RETURN_NONE;
}
// Otherwise return as bytes
auto pixels = screenshot.getPixelsPtr();
auto size = screenshot.getSize();
return PyBytes_FromStringAndSize((const char*)pixels, size.x * size.y * 4);
}
PyObject* PyWindow::get_game_resolution(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
auto resolution = game->getGameResolution();
return Py_BuildValue("(ii)", resolution.x, resolution.y);
}
int PyWindow::set_game_resolution(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
int width, height;
if (!PyArg_ParseTuple(value, "ii", &width, &height)) {
PyErr_SetString(PyExc_TypeError, "game_resolution must be a tuple of two integers (width, height)");
return -1;
}
if (width <= 0 || height <= 0) {
PyErr_SetString(PyExc_ValueError, "Game resolution dimensions must be positive");
return -1;
}
game->setGameResolution(width, height);
return 0;
}
PyObject* PyWindow::get_scaling_mode(PyWindowObject* self, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return NULL;
}
return PyUnicode_FromString(game->getViewportModeString().c_str());
}
int PyWindow::set_scaling_mode(PyWindowObject* self, PyObject* value, void* closure)
{
GameEngine* game = McRFPy_API::game;
if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1;
}
const char* mode_str = PyUnicode_AsUTF8(value);
if (!mode_str) {
PyErr_SetString(PyExc_TypeError, "scaling_mode must be a string");
return -1;
}
GameEngine::ViewportMode mode;
if (strcmp(mode_str, "center") == 0) {
mode = GameEngine::ViewportMode::Center;
} else if (strcmp(mode_str, "stretch") == 0) {
mode = GameEngine::ViewportMode::Stretch;
} else if (strcmp(mode_str, "fit") == 0) {
mode = GameEngine::ViewportMode::Fit;
} else {
PyErr_SetString(PyExc_ValueError, "scaling_mode must be 'center', 'stretch', or 'fit'");
return -1;
}
game->setViewportMode(mode);
return 0;
}
// Property definitions
PyGetSetDef PyWindow::getsetters[] = {
{"resolution", (getter)get_resolution, (setter)set_resolution,
MCRF_PROPERTY(resolution, "Window resolution as (width, height) tuple. Setting this recreates the window."), NULL},
{"fullscreen", (getter)get_fullscreen, (setter)set_fullscreen,
MCRF_PROPERTY(fullscreen, "Window fullscreen state (bool). Setting this recreates the window."), NULL},
{"vsync", (getter)get_vsync, (setter)set_vsync,
MCRF_PROPERTY(vsync, "Vertical sync enabled state (bool). Prevents screen tearing but may limit framerate."), NULL},
{"title", (getter)get_title, (setter)set_title,
MCRF_PROPERTY(title, "Window title string (str). Displayed in the window title bar."), NULL},
{"visible", (getter)get_visible, (setter)set_visible,
MCRF_PROPERTY(visible, "Window visibility state (bool). Hidden windows still process events."), NULL},
{"framerate_limit", (getter)get_framerate_limit, (setter)set_framerate_limit,
MCRF_PROPERTY(framerate_limit, "Frame rate limit in FPS (int, 0 for unlimited). Caps maximum frame rate."), NULL},
{"game_resolution", (getter)get_game_resolution, (setter)set_game_resolution,
MCRF_PROPERTY(game_resolution, "Fixed game resolution as (width, height) tuple. Enables resolution-independent rendering with scaling."), NULL},
{"scaling_mode", (getter)get_scaling_mode, (setter)set_scaling_mode,
MCRF_PROPERTY(scaling_mode, "Viewport scaling mode (str): 'center' (no scaling), 'stretch' (fill window), or 'fit' (maintain aspect ratio)."), NULL},
{NULL}
};
// Method definitions
PyMethodDef PyWindow::methods[] = {
{"get", (PyCFunction)PyWindow::get, METH_VARARGS | METH_CLASS,
MCRF_METHOD(Window, get,
MCRF_SIG("()", "Window"),
MCRF_DESC("Get the Window singleton instance."),
MCRF_RETURNS("Window: The global window object")
MCRF_NOTE("This is a class method. Call as Window.get(). There is only one window instance per application.")
)},
{"center", (PyCFunction)PyWindow::center, METH_NOARGS,
MCRF_METHOD(Window, center,
MCRF_SIG("()", "None"),
MCRF_DESC("Center the window on the screen."),
MCRF_RETURNS("None")
MCRF_NOTE("Only works in windowed mode. Has no effect when fullscreen or in headless mode.")
)},
{"screenshot", (PyCFunction)PyWindow::screenshot, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Window, screenshot,
MCRF_SIG("(filename: str = None)", "bytes | None"),
MCRF_DESC("Take a screenshot of the current window contents."),
MCRF_ARGS_START
MCRF_ARG("filename", "Optional path to save screenshot. If omitted, returns raw RGBA bytes.")
MCRF_RETURNS("bytes | None: Raw RGBA pixel data if no filename given, otherwise None after saving")
MCRF_NOTE("Screenshot is taken at the actual window resolution. Use after render loop update for current frame.")
)},
{NULL}
};

View File

@ -1,69 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
// Forward declarations
class GameEngine;
// Python object structure for Window singleton
typedef struct {
PyObject_HEAD
// No data - Window is a singleton that accesses GameEngine
} PyWindowObject;
// C++ interface for the Window singleton
class PyWindow
{
public:
// Static methods for Python type
static PyObject* get(PyObject* cls, PyObject* args);
static PyObject* repr(PyWindowObject* self);
// Getters and setters for window properties
static PyObject* get_resolution(PyWindowObject* self, void* closure);
static int set_resolution(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_fullscreen(PyWindowObject* self, void* closure);
static int set_fullscreen(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_vsync(PyWindowObject* self, void* closure);
static int set_vsync(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_title(PyWindowObject* self, void* closure);
static int set_title(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_visible(PyWindowObject* self, void* closure);
static int set_visible(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_framerate_limit(PyWindowObject* self, void* closure);
static int set_framerate_limit(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_game_resolution(PyWindowObject* self, void* closure);
static int set_game_resolution(PyWindowObject* self, PyObject* value, void* closure);
static PyObject* get_scaling_mode(PyWindowObject* self, void* closure);
static int set_scaling_mode(PyWindowObject* self, PyObject* value, void* closure);
// Methods
static PyObject* center(PyWindowObject* self, PyObject* args);
static PyObject* screenshot(PyWindowObject* self, PyObject* args, PyObject* kwds);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
static PyTypeObject PyWindowType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Window",
.tp_basicsize = sizeof(PyWindowObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self) {
// Don't delete the singleton instance
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)PyWindow::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Window singleton for accessing and modifying the game window properties"),
.tp_methods = nullptr, // Set in McRFPy_API.cpp after definition
.tp_getset = nullptr, // Set in McRFPy_API.cpp after definition
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
PyErr_SetString(PyExc_TypeError, "Cannot instantiate Window. Use Window.get() to access the singleton.");
return NULL;
}
};
}

View File

@ -1,85 +0,0 @@
#include "PythonObjectCache.h"
#include <iostream>
PythonObjectCache* PythonObjectCache::instance = nullptr;
PythonObjectCache& PythonObjectCache::getInstance() {
if (!instance) {
instance = new PythonObjectCache();
}
return *instance;
}
PythonObjectCache::~PythonObjectCache() {
clear();
}
uint64_t PythonObjectCache::assignSerial() {
return next_serial.fetch_add(1, std::memory_order_relaxed);
}
void PythonObjectCache::registerObject(uint64_t serial, PyObject* weakref) {
if (!weakref || serial == 0) return;
std::lock_guard<std::mutex> lock(serial_mutex);
// Clean up any existing entry
auto it = cache.find(serial);
if (it != cache.end()) {
Py_DECREF(it->second);
}
// Store the new weak reference
Py_INCREF(weakref);
cache[serial] = weakref;
}
PyObject* PythonObjectCache::lookup(uint64_t serial) {
if (serial == 0) return nullptr;
// No mutex needed for read - GIL protects PyWeakref_GetObject
auto it = cache.find(serial);
if (it != cache.end()) {
PyObject* obj = PyWeakref_GetObject(it->second);
if (obj && obj != Py_None) {
Py_INCREF(obj);
return obj;
}
}
return nullptr;
}
void PythonObjectCache::remove(uint64_t serial) {
if (serial == 0) return;
std::lock_guard<std::mutex> lock(serial_mutex);
auto it = cache.find(serial);
if (it != cache.end()) {
Py_DECREF(it->second);
cache.erase(it);
}
}
void PythonObjectCache::cleanup() {
std::lock_guard<std::mutex> lock(serial_mutex);
auto it = cache.begin();
while (it != cache.end()) {
PyObject* obj = PyWeakref_GetObject(it->second);
if (!obj || obj == Py_None) {
Py_DECREF(it->second);
it = cache.erase(it);
} else {
++it;
}
}
}
void PythonObjectCache::clear() {
std::lock_guard<std::mutex> lock(serial_mutex);
for (auto& pair : cache) {
Py_DECREF(pair.second);
}
cache.clear();
}

View File

@ -1,40 +0,0 @@
#pragma once
#include <Python.h>
#include <unordered_map>
#include <mutex>
#include <atomic>
#include <cstdint>
class PythonObjectCache {
private:
static PythonObjectCache* instance;
std::mutex serial_mutex;
std::atomic<uint64_t> next_serial{1};
std::unordered_map<uint64_t, PyObject*> cache;
PythonObjectCache() = default;
~PythonObjectCache();
public:
static PythonObjectCache& getInstance();
// Assign a new serial number
uint64_t assignSerial();
// Register a Python object with a serial number
void registerObject(uint64_t serial, PyObject* weakref);
// Lookup a Python object by serial number
// Returns new reference or nullptr
PyObject* lookup(uint64_t serial);
// Remove an entry from the cache
void remove(uint64_t serial);
// Clean up dead weak references
void cleanup();
// Clear entire cache (for module cleanup)
void clear();
};

View File

@ -30,6 +30,16 @@ std::string Scene::action(int code)
return actions[code]; return actions[code];
} }
bool Scene::registerActionInjected(int code, std::string name)
{
std::cout << "Inject registered action - default implementation\n";
return false;
}
bool Scene::unregisterActionInjected(int code, std::string name)
{
return false;
}
void Scene::key_register(PyObject* callable) void Scene::key_register(PyObject* callable)
{ {
@ -54,43 +64,3 @@ void Scene::key_unregister()
*/ */
key_callable.reset(); key_callable.reset();
} }
// #118: Scene animation property support
bool Scene::setProperty(const std::string& name, float value)
{
if (name == "x") {
position.x = value;
return true;
}
if (name == "y") {
position.y = value;
return true;
}
if (name == "opacity") {
opacity = std::max(0.0f, std::min(1.0f, value));
return true;
}
if (name == "visible") {
visible = (value != 0.0f);
return true;
}
return false;
}
bool Scene::setProperty(const std::string& name, const sf::Vector2f& value)
{
if (name == "pos" || name == "position") {
position = value;
return true;
}
return false;
}
float Scene::getProperty(const std::string& name) const
{
if (name == "x") return position.x;
if (name == "y") return position.y;
if (name == "opacity") return opacity;
if (name == "visible") return visible ? 1.0f : 0.0f;
return 0.0f;
}

View File

@ -4,6 +4,7 @@
#define ACTION(X, Y) (name.compare(X) == 0 && type.compare(Y) == 0) #define ACTION(X, Y) (name.compare(X) == 0 && type.compare(Y) == 0)
#define ACTIONONCE(X) ((name.compare(X) == 0 && type.compare("start") == 0 && !actionState[name])) #define ACTIONONCE(X) ((name.compare(X) == 0 && type.compare("start") == 0 && !actionState[name]))
#define ACTIONAFTER(X) ((name.compare(X) == 0 && type.compare("end") == 0)) #define ACTIONAFTER(X) ((name.compare(X) == 0 && type.compare("end") == 0))
#define ACTIONPY ((name.size() > 3 && name.compare(name.size() - 3, 3, "_py") == 0))
#include "Common.h" #include "Common.h"
#include <list> #include <list>
@ -36,6 +37,8 @@ public:
bool hasAction(int); bool hasAction(int);
std::string action(int); std::string action(int);
virtual bool registerActionInjected(int, std::string);
virtual bool unregisterActionInjected(int, std::string);
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> ui_elements; std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> ui_elements;
@ -43,14 +46,4 @@ public:
std::unique_ptr<PyKeyCallable> key_callable; std::unique_ptr<PyKeyCallable> key_callable;
void key_register(PyObject*); void key_register(PyObject*);
void key_unregister(); void key_unregister();
// #118: Scene-level UIDrawable-like properties for animations/transitions
sf::Vector2f position{0.0f, 0.0f}; // Offset applied to all ui_elements
bool visible = true; // Controls rendering of scene
float opacity = 1.0f; // Applied to all ui_elements (0.0-1.0)
// Animation support for scene properties
bool setProperty(const std::string& name, float value);
bool setProperty(const std::string& name, const sf::Vector2f& value);
float getProperty(const std::string& name) const;
}; };

View File

@ -1,85 +0,0 @@
#include "SceneTransition.h"
void SceneTransition::start(TransitionType t, const std::string& from, const std::string& to, float dur) {
type = t;
fromScene = from;
toScene = to;
duration = dur;
elapsed = 0.0f;
// Initialize render textures if needed
if (!oldSceneTexture) {
oldSceneTexture = std::make_unique<sf::RenderTexture>();
oldSceneTexture->create(1024, 768);
}
if (!newSceneTexture) {
newSceneTexture = std::make_unique<sf::RenderTexture>();
newSceneTexture->create(1024, 768);
}
}
void SceneTransition::update(float dt) {
if (type == TransitionType::None) return;
elapsed += dt;
}
void SceneTransition::render(sf::RenderTarget& target) {
if (type == TransitionType::None) return;
float progress = getProgress();
float easedProgress = easeInOut(progress);
// Update sprites with current textures
oldSprite.setTexture(oldSceneTexture->getTexture());
newSprite.setTexture(newSceneTexture->getTexture());
switch (type) {
case TransitionType::Fade:
// Fade out old scene, fade in new scene
oldSprite.setColor(sf::Color(255, 255, 255, 255 * (1.0f - easedProgress)));
newSprite.setColor(sf::Color(255, 255, 255, 255 * easedProgress));
target.draw(oldSprite);
target.draw(newSprite);
break;
case TransitionType::SlideLeft:
// Old scene slides out to left, new scene slides in from right
oldSprite.setPosition(-1024 * easedProgress, 0);
newSprite.setPosition(1024 * (1.0f - easedProgress), 0);
target.draw(oldSprite);
target.draw(newSprite);
break;
case TransitionType::SlideRight:
// Old scene slides out to right, new scene slides in from left
oldSprite.setPosition(1024 * easedProgress, 0);
newSprite.setPosition(-1024 * (1.0f - easedProgress), 0);
target.draw(oldSprite);
target.draw(newSprite);
break;
case TransitionType::SlideUp:
// Old scene slides up, new scene slides in from bottom
oldSprite.setPosition(0, -768 * easedProgress);
newSprite.setPosition(0, 768 * (1.0f - easedProgress));
target.draw(oldSprite);
target.draw(newSprite);
break;
case TransitionType::SlideDown:
// Old scene slides down, new scene slides in from top
oldSprite.setPosition(0, 768 * easedProgress);
newSprite.setPosition(0, -768 * (1.0f - easedProgress));
target.draw(oldSprite);
target.draw(newSprite);
break;
default:
break;
}
}
float SceneTransition::easeInOut(float t) {
// Smooth ease-in-out curve
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}

View File

@ -1,42 +0,0 @@
#pragma once
#include "Common.h"
#include <SFML/Graphics.hpp>
#include <string>
#include <memory>
enum class TransitionType {
None,
Fade,
SlideLeft,
SlideRight,
SlideUp,
SlideDown
};
class SceneTransition {
public:
TransitionType type = TransitionType::None;
float duration = 0.0f;
float elapsed = 0.0f;
std::string fromScene;
std::string toScene;
// Render textures for transition
std::unique_ptr<sf::RenderTexture> oldSceneTexture;
std::unique_ptr<sf::RenderTexture> newSceneTexture;
// Sprites for rendering textures
sf::Sprite oldSprite;
sf::Sprite newSprite;
SceneTransition() = default;
void start(TransitionType t, const std::string& from, const std::string& to, float dur);
void update(float dt);
void render(sf::RenderTarget& target);
bool isComplete() const { return elapsed >= duration; }
float getProgress() const { return duration > 0 ? std::min(elapsed / duration, 1.0f) : 1.0f; }
// Easing function for smooth transitions
static float easeInOut(float t);
};

View File

@ -1,147 +1,31 @@
#include "Timer.h" #include "Timer.h"
#include "PythonObjectCache.h"
#include "PyCallable.h"
#include "McRFPy_API.h"
#include "GameEngine.h"
Timer::Timer(PyObject* _target, int _interval, int now, bool _once) Timer::Timer(PyObject* _target, int _interval, int now)
: callback(std::make_shared<PyCallable>(_target)), interval(_interval), last_ran(now), : target(_target), interval(_interval), last_ran(now)
paused(false), pause_start_time(0), total_paused_time(0), once(_once)
{} {}
Timer::Timer() Timer::Timer()
: callback(std::make_shared<PyCallable>(Py_None)), interval(0), last_ran(0), : target(Py_None), interval(0), last_ran(0)
paused(false), pause_start_time(0), total_paused_time(0), once(false)
{} {}
Timer::~Timer() {
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
}
bool Timer::hasElapsed(int now) const
{
if (paused) return false;
return now >= last_ran + interval;
}
bool Timer::test(int now) bool Timer::test(int now)
{ {
if (!callback || callback->isNone()) return false; if (!target || target == Py_None) return false;
if (now > last_ran + interval)
if (hasElapsed(now))
{ {
last_ran = now; last_ran = now;
PyObject* args = Py_BuildValue("(i)", now);
// Get the PyTimer wrapper from cache to pass to callback PyObject* retval = PyObject_Call(target, args, NULL);
PyObject* timer_obj = nullptr;
if (serial_number != 0) {
timer_obj = PythonObjectCache::getInstance().lookup(serial_number);
}
// Build args: (timer, runtime) or just (runtime) if no wrapper found
PyObject* args;
if (timer_obj) {
args = Py_BuildValue("(Oi)", timer_obj, now);
} else {
// Fallback to old behavior if no wrapper found
args = Py_BuildValue("(i)", now);
}
PyObject* retval = callback->call(args, NULL);
Py_DECREF(args);
if (!retval) if (!retval)
{ {
std::cerr << "Timer callback raised an exception:" << std::endl; std::cout << "timer has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print(); PyErr_Print();
PyErr_Clear(); PyErr_Clear();
// Check if we should exit on exception
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
McRFPy_API::signalPythonException();
}
} else if (retval != Py_None) } else if (retval != Py_None)
{ {
std::cout << "Timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
Py_DECREF(retval);
} }
// Handle one-shot timers
if (once) {
cancel();
}
return true; return true;
} }
return false; return false;
} }
void Timer::pause(int current_time)
{
if (!paused) {
paused = true;
pause_start_time = current_time;
}
}
void Timer::resume(int current_time)
{
if (paused) {
paused = false;
int paused_duration = current_time - pause_start_time;
total_paused_time += paused_duration;
// Adjust last_ran to account for the pause
last_ran += paused_duration;
}
}
void Timer::restart(int current_time)
{
last_ran = current_time;
paused = false;
pause_start_time = 0;
total_paused_time = 0;
}
void Timer::cancel()
{
// Cancel by setting callback to None
callback = std::make_shared<PyCallable>(Py_None);
}
bool Timer::isActive() const
{
return callback && !callback->isNone() && !paused;
}
int Timer::getRemaining(int current_time) const
{
if (paused) {
// When paused, calculate time remaining from when it was paused
int elapsed_when_paused = pause_start_time - last_ran;
return interval - elapsed_when_paused;
}
int elapsed = current_time - last_ran;
return interval - elapsed;
}
int Timer::getElapsed(int current_time) const
{
if (paused) {
return pause_start_time - last_ran;
}
return current_time - last_ran;
}
PyObject* Timer::getCallback()
{
if (!callback) return Py_None;
return callback->borrow();
}
void Timer::setCallback(PyObject* new_callback)
{
callback = std::make_shared<PyCallable>(new_callback);
}

Some files were not shown because too many files have changed in this diff Show More