Compare commits
5 Commits
master
...
grid_entit
| Author | SHA1 | Date |
|---|---|---|
|
|
d93311fea8 | |
|
|
b920a51736 | |
|
|
c975599251 | |
|
|
1d852f875b | |
|
|
3b86089128 |
|
|
@ -8,26 +8,5 @@ PCbuild
|
|||
obj
|
||||
build
|
||||
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/
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
[submodule "modules/SFML"]
|
||||
path = modules/SFML
|
||||
url = git@github.com:SFML/SFML.git
|
||||
[submodule "modules/libtcod-headless"]
|
||||
path = modules/libtcod-headless
|
||||
url = git@github.com:jmccardle/libtcod-headless.git
|
||||
branch = 2.2.1-headless
|
||||
[submodule "modules/libtcod"]
|
||||
path = modules/libtcod
|
||||
url = git@github.com:libtcod/libtcod.git
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
626
CLAUDE.md
|
|
@ -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).
|
||||
|
|
@ -17,69 +17,40 @@ include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
|
|||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
|
||||
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
|
||||
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
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod
|
||||
OpenGL::GL)
|
||||
set(LINK_LIBS
|
||||
m
|
||||
dl
|
||||
util
|
||||
pthread
|
||||
python3.12
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod)
|
||||
|
||||
# On Windows, add any additional libs and include directories
|
||||
if(WIN32)
|
||||
# Windows-specific Python library name (no dots)
|
||||
list(APPEND LINK_LIBS python314)
|
||||
# Add the necessary Windows-specific libraries and include directories
|
||||
# include_directories(path_to_additional_includes)
|
||||
# link_directories(path_to_additional_libs)
|
||||
# list(APPEND LINK_LIBS additional_windows_libs)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
else()
|
||||
# Unix/Linux specific libraries
|
||||
list(APPEND LINK_LIBS python3.14 m dl util pthread)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
endif()
|
||||
|
||||
# Add the directory where the linker should look for the libraries
|
||||
#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
|
||||
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
|
||||
target_link_libraries(mcrogueface ${LINK_LIBS})
|
||||
|
||||
|
|
@ -96,28 +67,9 @@ add_custom_command(TARGET mcrogueface POST_BUILD
|
|||
# Copy Python standard library to build directory
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
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
|
||||
if(WIN32)
|
||||
# Copy all DLL files from lib to the executable directory
|
||||
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()
|
||||
# rpath for including shared libraries
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
INSTALL_RPATH "./lib")
|
||||
|
||||
|
|
|
|||
54
GNUmakefile
54
GNUmakefile
|
|
@ -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
198
README.md
|
|
@ -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*
|
||||
|
||||
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
|
||||
* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera
|
||||
* Simple GUI element system allows keyboard and mouse input, composition
|
||||
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship"
|
||||
* C++ first, Python close behind.
|
||||
* Entity-Component system based on David Churchill's Memorial University COMP4300 course lectures available on Youtube.
|
||||
* Graphics, particles and shaders provided by SFML.
|
||||
* 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**:
|
||||
|
||||
- The entire McRogueFace visual framework:
|
||||
- **Sprite**: an image file or one sprite from a shared sprite sheet
|
||||
- **Caption**: load a font, display text
|
||||
- **Frame**: A rectangle; put other things on it to move or manage GUIs as modules
|
||||
- **Grid**: A 2D array of tiles with zoom + position control
|
||||
- **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path
|
||||
- **Animation**: Change any property on any of the above over time
|
||||
|
||||
```bash
|
||||
# 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
|
||||
* ✅ Initial Commit
|
||||
* ✅ Integrate scene, action, entity, component system from COMP4300 engine
|
||||
* ✅ Windows / Visual Studio project
|
||||
* ✅ Draw Sprites
|
||||
* ✅ Play Sounds
|
||||
* ✅ Draw UI, spawn entity from Python code
|
||||
* ❌ Python AI for entities (NPCs on set paths, enemies towards player)
|
||||
* ✅ Walking / Collision
|
||||
* ❌ "Boards" (stairs / doors / walk off edge of screen)
|
||||
* ❌ Cutscenes - interrupt normal controls, text scroll, character portraits
|
||||
* ❌ Mouse integration - tooltips, zoom, click to select targets, cursors
|
||||
|
|
|
|||
223
ROADMAP.md
223
ROADMAP.md
|
|
@ -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.
54
build.sh
54
build.sh
|
|
@ -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 ."
|
||||
|
|
@ -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 ..
|
||||
|
|
@ -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 ..
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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
1070
docs/mcrfpy.3
1070
docs/mcrfpy.3
File diff suppressed because it is too large
Load Diff
|
|
@ -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."""
|
||||
...
|
||||
|
|
@ -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
|
||||
|
|
@ -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: ...
|
||||
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
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 34ae258a863c4f6446effee28ca8ecae51b1519f
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 3b4b65dc9aae7d21a98d3578e3e9433728b118bb
|
||||
|
|
@ -11,10 +11,10 @@ public:
|
|||
const static int WHEEL_NUM = 4;
|
||||
const static int WHEEL_NEG = 2;
|
||||
const static int WHEEL_DEL = 1;
|
||||
static int keycode(const sf::Keyboard::Key& k) { return KEY + (int)k; }
|
||||
static int keycode(const sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
|
||||
static int keycode(sf::Keyboard::Key& k) { return KEY + (int)k; }
|
||||
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(const sf::Mouse::Wheel& w, float d) {
|
||||
static int keycode(sf::Mouse::Wheel& w, float d) {
|
||||
int neg = 0;
|
||||
if (d < 0) { neg = 1; }
|
||||
return MOUSEWHEEL + (w * WHEEL_NUM) + (neg * WHEEL_NEG) + 1;
|
||||
|
|
@ -32,7 +32,7 @@ public:
|
|||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
175
src/Animation.h
175
src/Animation.h
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -4,357 +4,70 @@
|
|||
#include "PyScene.h"
|
||||
#include "UITestScene.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(const McRogueFaceConfig& cfg)
|
||||
: config(cfg), headless(cfg.headless)
|
||||
GameEngine::GameEngine()
|
||||
{
|
||||
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
|
||||
Resources::game = this;
|
||||
window_title = "McRogueFace Engine";
|
||||
|
||||
// Initialize rendering based on headless mode
|
||||
if (headless) {
|
||||
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();
|
||||
window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine";
|
||||
window.create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close);
|
||||
visible = window.getDefaultView();
|
||||
window.setFramerateLimit(60);
|
||||
scene = "uitest";
|
||||
scenes["uitest"] = new UITestScene(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::executePyString("import mcrfpy");
|
||||
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.
|
||||
McRFPy_API::api_init();
|
||||
McRFPy_API::executePyString("import mcrfpy");
|
||||
McRFPy_API::executeScript("scripts/game.py");
|
||||
|
||||
clock.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::getScene(const std::string& name) {
|
||||
auto it = scenes.find(name);
|
||||
return (it != scenes.end()) ? it->second : nullptr;
|
||||
}
|
||||
void GameEngine::changeScene(std::string s)
|
||||
{
|
||||
changeScene(s, TransitionType::None, 0.0f);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
/*std::cout << "Current scene is now '" << s << "'\n";*/
|
||||
if (scenes.find(s) != scenes.end())
|
||||
scene = s;
|
||||
else
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
std::cout << "Attempted to change to a scene that doesn't exist (`" << s << "`)" << std::endl;
|
||||
}
|
||||
void GameEngine::quit() { running = false; }
|
||||
void GameEngine::setPause(bool p) { paused = p; }
|
||||
sf::Font & GameEngine::getFont() { /*return font; */ return Resources::font; }
|
||||
sf::RenderWindow & GameEngine::getWindow() {
|
||||
if (!window) {
|
||||
throw std::runtime_error("Window not available in headless mode");
|
||||
}
|
||||
return *window;
|
||||
}
|
||||
|
||||
sf::RenderTarget & GameEngine::getRenderTarget() {
|
||||
return *render_target;
|
||||
}
|
||||
sf::RenderWindow & GameEngine::getWindow() { return window; }
|
||||
|
||||
void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); }
|
||||
|
||||
void GameEngine::setWindowScale(float multiplier)
|
||||
{
|
||||
if (!headless && window) {
|
||||
window->setSize(sf::Vector2u(gameResolution.x * multiplier, gameResolution.y * multiplier));
|
||||
updateViewport();
|
||||
}
|
||||
window.setSize(sf::Vector2u(1024 * multiplier, 768 * multiplier)); // 7DRL 2024: window scaling
|
||||
//window.create(sf::VideoMode(1024 * multiplier, 768 * multiplier), window_title, sf::Style::Titlebar | sf::Style::Close);
|
||||
}
|
||||
|
||||
void GameEngine::run()
|
||||
{
|
||||
//std::cout << "GameEngine::run() starting main loop..." << std::endl;
|
||||
float fps = 0.0;
|
||||
frameTime = 0.016f; // Initialize to ~60 FPS
|
||||
clock.restart();
|
||||
while (running)
|
||||
{
|
||||
// Reset per-frame metrics
|
||||
metrics.resetPerFrame();
|
||||
|
||||
currentScene()->update();
|
||||
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();
|
||||
|
||||
// Update ImGui
|
||||
if (imguiInitialized) {
|
||||
ImGui::SFML::Update(*window, clock.getElapsedTime());
|
||||
}
|
||||
}
|
||||
sUserInput();
|
||||
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();
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
currentScene()->render();
|
||||
currentFrame++;
|
||||
frameTime = clock.restart().asSeconds();
|
||||
fps = 1 / frameTime;
|
||||
|
||||
// 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;
|
||||
}
|
||||
int whole_fps = (int)fps;
|
||||
int tenth_fps = int(fps * 100) % 10;
|
||||
//window.setTitle(window_title + " " + std::to_string(fps) + " FPS");
|
||||
window.setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
@ -366,7 +79,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
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -375,7 +88,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;
|
||||
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()
|
||||
|
|
@ -384,15 +97,9 @@ void GameEngine::testTimers()
|
|||
auto it = timers.begin();
|
||||
while (it != timers.end())
|
||||
{
|
||||
// Keep a local copy of the timer to prevent use-after-free.
|
||||
// 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.
|
||||
// Note: Check it->second (current map value) in case callback replaced it.
|
||||
if (!it->second->getCallback() || it->second->getCallback() == Py_None)
|
||||
it->second->test(now);
|
||||
|
||||
if (it->second->isNone())
|
||||
{
|
||||
it = timers.erase(it);
|
||||
}
|
||||
|
|
@ -401,95 +108,86 @@ void GameEngine::testTimers()
|
|||
}
|
||||
}
|
||||
|
||||
void GameEngine::processEvent(const sf::Event& event)
|
||||
{
|
||||
std::string actionType;
|
||||
int actionCode = 0;
|
||||
|
||||
if (event.type == sf::Event::Closed) { running = false; return; }
|
||||
|
||||
// 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) {
|
||||
// Update the viewport to handle the new window size
|
||||
updateViewport();
|
||||
|
||||
// Notify Python scenes about the resize
|
||||
McRFPy_API::triggerResize(event.size.width, event.size.height);
|
||||
}
|
||||
|
||||
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::KeyReleased || event.type == sf::Event::MouseButtonReleased) actionType = "end";
|
||||
|
||||
if (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseButtonReleased)
|
||||
actionCode = ActionCode::keycode(event.mouseButton.button);
|
||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
|
||||
actionCode = ActionCode::keycode(event.key.code);
|
||||
else if (event.type == sf::Event::MouseWheelScrolled)
|
||||
{
|
||||
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
|
||||
{
|
||||
int delta = 1;
|
||||
if (event.mouseWheelScroll.delta < 0) delta = -1;
|
||||
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
|
||||
}
|
||||
}
|
||||
// #140 - Handle mouse movement for hover detection
|
||||
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
|
||||
return;
|
||||
|
||||
if (currentScene()->hasAction(actionCode))
|
||||
{
|
||||
std::string name = currentScene()->action(actionCode);
|
||||
currentScene()->doAction(name, actionType);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void GameEngine::sUserInput()
|
||||
{
|
||||
sf::Event event;
|
||||
while (window && window->pollEvent(event))
|
||||
while (window.pollEvent(event))
|
||||
{
|
||||
// Process event through ImGui first
|
||||
if (imguiInitialized) {
|
||||
ImGui::SFML::ProcessEvent(*window, event);
|
||||
std::string actionType;
|
||||
int actionCode = 0;
|
||||
|
||||
if (event.type == sf::Event::Closed) { running = false; continue; }
|
||||
// TODO: add resize event to Scene to react; call it after constructor too, maybe
|
||||
else if (event.type == sf::Event::Resized) {
|
||||
continue; // 7DRL short circuit. Resizing manually disabled
|
||||
/*
|
||||
sf::FloatRect area(0.f, 0.f, event.size.width, event.size.height);
|
||||
//sf::FloatRect area(0.f, 0.f, 1024.f, 768.f); // 7DRL 2024: attempt to set scale appropriately
|
||||
//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
|
||||
*/
|
||||
}
|
||||
|
||||
// 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 (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start";
|
||||
else if (event.type == sf::Event::KeyReleased || event.type == sf::Event::MouseButtonReleased) actionType = "end";
|
||||
|
||||
// If console wants keyboard, don't pass keyboard events to game
|
||||
if (console.wantsKeyboardInput()) {
|
||||
// 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;
|
||||
if (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseButtonReleased)
|
||||
actionCode = ActionCode::keycode(event.mouseButton.button);
|
||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
|
||||
actionCode = ActionCode::keycode(event.key.code);
|
||||
else if (event.type == sf::Event::MouseWheelScrolled)
|
||||
{
|
||||
// //sf::Mouse::Wheel w = event.MouseWheelScrollEvent.wheel;
|
||||
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
|
||||
{
|
||||
int delta = 1;
|
||||
if (event.mouseWheelScroll.delta < 0) delta = -1;
|
||||
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;
|
||||
// actionCode = ActionCode::keycode(0, d);
|
||||
}
|
||||
else
|
||||
continue;
|
||||
|
||||
processEvent(event);
|
||||
//std::cout << "Event produced action code " << actionCode << ": " << actionType << std::endl;
|
||||
|
||||
if (currentScene()->hasAction(actionCode))
|
||||
{
|
||||
std::string name = currentScene()->action(actionCode);
|
||||
currentScene()->doAction(name, actionType);
|
||||
}
|
||||
else if (currentScene()->key_callable)
|
||||
{
|
||||
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);
|
||||
if (!retval)
|
||||
{
|
||||
std::cout << "key_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 << "key_callable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
}
|
||||
*/
|
||||
}
|
||||
else
|
||||
{
|
||||
//std::cout << "[GameEngine] Action not registered for input: " << actionCode << ": " << actionType << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -510,123 +208,3 @@ std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> GameEngine::scene_ui(s
|
|||
if (scenes.count(target) == 0) return NULL;
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
181
src/GameEngine.h
181
src/GameEngine.h
|
|
@ -6,97 +6,10 @@
|
|||
#include "IndexTexture.h"
|
||||
#include "Timer.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
|
||||
{
|
||||
public:
|
||||
// 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::RenderWindow window;
|
||||
sf::Font font;
|
||||
std::map<std::string, Scene*> scenes;
|
||||
bool running = true;
|
||||
|
|
@ -106,88 +19,29 @@ private:
|
|||
sf::Clock clock;
|
||||
float frameTime;
|
||||
std::string window_title;
|
||||
|
||||
bool headless = false;
|
||||
McRogueFaceConfig config;
|
||||
bool cleaned_up = false;
|
||||
|
||||
// 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();
|
||||
|
||||
sf::Clock runtime;
|
||||
//std::map<std::string, Timer> timers;
|
||||
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
|
||||
void testTimers();
|
||||
|
||||
public:
|
||||
sf::Clock runtime;
|
||||
std::map<std::string, std::shared_ptr<Timer>> timers;
|
||||
std::string scene;
|
||||
|
||||
// Profiling metrics (struct defined above class)
|
||||
ProfilingMetrics metrics;
|
||||
|
||||
GameEngine();
|
||||
GameEngine(const McRogueFaceConfig& cfg);
|
||||
~GameEngine();
|
||||
Scene* currentScene();
|
||||
Scene* getScene(const std::string& name); // #118: Get scene by name
|
||||
void changeScene(std::string);
|
||||
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
|
||||
void createScene(std::string);
|
||||
void quit();
|
||||
void setPause(bool);
|
||||
sf::Font & getFont();
|
||||
sf::RenderWindow & getWindow();
|
||||
sf::RenderTarget & getRenderTarget();
|
||||
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
|
||||
void run();
|
||||
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; }
|
||||
float getFrameTime() { return frameTime; }
|
||||
sf::View getView() { return visible; }
|
||||
void manageTimer(std::string, PyObject*, int);
|
||||
std::shared_ptr<Timer> getTimer(const std::string& name);
|
||||
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
|
||||
std::vector<IndexTexture> textures;
|
||||
|
|
@ -197,30 +51,5 @@ public:
|
|||
sf::Music music;
|
||||
sf::Sound sfx;
|
||||
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);
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
108
src/GridChunk.h
108
src/GridChunk.h
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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());
|
||||
}
|
||||
244
src/GridLayers.h
244
src/GridLayers.h
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
1199
src/McRFPy_API.cpp
1199
src/McRFPy_API.cpp
File diff suppressed because it is too large
Load Diff
|
|
@ -2,11 +2,9 @@
|
|||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <list>
|
||||
#include <atomic>
|
||||
|
||||
#include "PyFont.h"
|
||||
#include "PyTexture.h"
|
||||
#include "McRogueFaceConfig.h"
|
||||
|
||||
class GameEngine; // forward declared (circular members)
|
||||
|
||||
|
|
@ -29,18 +27,19 @@ public:
|
|||
//static void setSpriteTexture(int);
|
||||
inline static GameEngine* game;
|
||||
static void api_init();
|
||||
static void api_init(const McRogueFaceConfig& config);
|
||||
static PyStatus init_python_with_config(const McRogueFaceConfig& config);
|
||||
static void api_shutdown();
|
||||
// Python API functionality - use mcrfpy.* in scripts
|
||||
//static PyObject* _drawSprite(PyObject*, PyObject*);
|
||||
static void REPL_device(FILE * fp, const char *filename);
|
||||
static void REPL();
|
||||
|
||||
static std::vector<sf::SoundBuffer>* soundbuffers;
|
||||
static sf::Music* music;
|
||||
static sf::Sound* sfx;
|
||||
static std::vector<sf::SoundBuffer> soundbuffers;
|
||||
static sf::Music music;
|
||||
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* _loadMusic(PyObject*, PyObject*);
|
||||
|
|
@ -67,37 +66,12 @@ public:
|
|||
|
||||
// accept keyboard input from scene
|
||||
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 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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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", ¤tX, ¤tY)) {
|
||||
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", ¤tX, ¤tY)) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
111
src/Profiler.h
111
src/Profiler.h
|
|
@ -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(); }
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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}
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,27 +1,10 @@
|
|||
#include "PyCallable.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "GameEngine.h"
|
||||
|
||||
PyCallable::PyCallable(PyObject* _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()
|
||||
{
|
||||
if (target)
|
||||
|
|
@ -33,11 +16,49 @@ PyObject* PyCallable::call(PyObject* args, PyObject* kwargs)
|
|||
return PyObject_Call(target, args, kwargs);
|
||||
}
|
||||
|
||||
bool PyCallable::isNone() const
|
||||
bool PyCallable::isNone()
|
||||
{
|
||||
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)
|
||||
: PyCallable(_target)
|
||||
|
|
@ -53,14 +74,9 @@ void PyClickCallable::call(sf::Vector2f mousepos, std::string button, std::strin
|
|||
PyObject* retval = PyCallable::call(args, NULL);
|
||||
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_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)
|
||||
{
|
||||
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);
|
||||
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_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)
|
||||
{
|
||||
std::cout << "KeyCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
|
|
|
|||
|
|
@ -6,15 +6,24 @@ class PyCallable
|
|||
{
|
||||
protected:
|
||||
PyObject* target;
|
||||
|
||||
public:
|
||||
PyCallable(PyObject*);
|
||||
PyCallable(const PyCallable& other);
|
||||
PyCallable& operator=(const PyCallable& other);
|
||||
~PyCallable();
|
||||
PyObject* call(PyObject*, PyObject*);
|
||||
bool isNone() const;
|
||||
PyObject* borrow() const { return target; }
|
||||
public:
|
||||
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
|
||||
|
|
@ -24,11 +33,6 @@ public:
|
|||
PyObject* borrow();
|
||||
PyClickCallable(PyObject*);
|
||||
PyClickCallable();
|
||||
PyClickCallable(const PyClickCallable& other) : PyCallable(other) {}
|
||||
PyClickCallable& operator=(const PyClickCallable& other) {
|
||||
PyCallable::operator=(other);
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
class PyKeyCallable: public PyCallable
|
||||
|
|
|
|||
246
src/PyColor.cpp
246
src/PyColor.cpp
|
|
@ -1,51 +1,11 @@
|
|||
#include "PyColor.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include "PyRAII.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
|
||||
PyGetSetDef PyColor::getsetters[] = {
|
||||
{"r", (getter)PyColor::get_member, (setter)PyColor::set_member,
|
||||
MCRF_PROPERTY(r, "Red component (0-255). Automatically clamped to valid range."), (void*)0},
|
||||
{"g", (getter)PyColor::get_member, (setter)PyColor::set_member,
|
||||
MCRF_PROPERTY(g, "Green component (0-255). Automatically clamped to valid range."), (void*)1},
|
||||
{"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")
|
||||
)},
|
||||
{"r", (getter)PyColor::get_member, (setter)PyColor::set_member, "Red component", (void*)0},
|
||||
{"g", (getter)PyColor::get_member, (setter)PyColor::set_member, "Green component", (void*)1},
|
||||
{"b", (getter)PyColor::get_member, (setter)PyColor::set_member, "Blue component", (void*)2},
|
||||
{"a", (getter)PyColor::get_member, (setter)PyColor::set_member, "Alpha component", (void*)3},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -54,16 +14,11 @@ PyColor::PyColor(sf::Color target)
|
|||
|
||||
PyObject* PyColor::pyObject()
|
||||
{
|
||||
PyTypeObject* type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
|
||||
if (!type) return nullptr;
|
||||
|
||||
PyColorObject* obj = (PyColorObject*)type->tp_alloc(type, 0);
|
||||
Py_DECREF(type);
|
||||
|
||||
if (obj) {
|
||||
obj->data = data;
|
||||
}
|
||||
return (PyObject*)obj;
|
||||
PyObject* obj = PyType_GenericAlloc(&mcrfpydef::PyColorType, 0);
|
||||
Py_INCREF(obj);
|
||||
PyColorObject* self = (PyColorObject*)obj;
|
||||
self->data = data;
|
||||
return 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)
|
||||
{
|
||||
PyColorObject* self = (PyColorObject*)obj;
|
||||
long member = (long)closure;
|
||||
|
||||
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;
|
||||
}
|
||||
// TODO
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
int PyColor::set_member(PyObject* obj, PyObject* value, void* closure)
|
||||
{
|
||||
PyColorObject* self = (PyColorObject*)obj;
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO
|
||||
return 0;
|
||||
}
|
||||
|
||||
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");
|
||||
if (!PyObject_IsInstance(other_obj, (PyObject*)type)) {
|
||||
Py_DECREF(type);
|
||||
PyErr_SetString(PyExc_TypeError, "First argument must be a Color");
|
||||
if (PyObject_IsInstance(args, (PyObject*)type)) return (PyColorObject*)args;
|
||||
auto obj = (PyColorObject*)type->tp_alloc(type, 0);
|
||||
int err = init(obj, args, NULL);
|
||||
if (err) {
|
||||
Py_DECREF(obj);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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;
|
||||
return obj;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,19 +28,12 @@ public:
|
|||
static PyObject* get_member(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 PyMethodDef methods[];
|
||||
static PyColorObject* from_arg(PyObject*);
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
static PyTypeObject PyColorType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Color",
|
||||
.tp_basicsize = sizeof(PyColorObject),
|
||||
.tp_itemsize = 0,
|
||||
|
|
@ -48,7 +41,6 @@ namespace mcrfpydef {
|
|||
.tp_hash = PyColor::hash,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("SFML Color Object"),
|
||||
.tp_methods = PyColor::methods,
|
||||
.tp_getset = PyColor::getsetters,
|
||||
.tp_init = (initproc)PyColor::init,
|
||||
.tp_new = PyColor::pynew,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
#include "PyFont.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,17 +21,10 @@ public:
|
|||
static Py_hash_t hash(PyObject*);
|
||||
static int init(PyFontObject*, PyObject*, PyObject*);
|
||||
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 {
|
||||
static PyTypeObject PyFontType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Font",
|
||||
.tp_basicsize = sizeof(PyFontObject),
|
||||
.tp_itemsize = 0,
|
||||
|
|
@ -39,7 +32,6 @@ namespace mcrfpydef {
|
|||
//.tp_hash = PyFont::hash,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("SFML Font Object"),
|
||||
.tp_getset = PyFont::getsetters,
|
||||
//.tp_base = &PyBaseObject_Type,
|
||||
.tp_init = (initproc)PyFont::init,
|
||||
.tp_new = PyType_GenericNew, //PyFont::pynew,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
};
|
||||
138
src/PyRAII.h
138
src/PyRAII.h
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
181
src/PyScene.cpp
181
src/PyScene.cpp
|
|
@ -2,11 +2,6 @@
|
|||
#include "ActionCode.h"
|
||||
#include "Resources.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)
|
||||
{
|
||||
|
|
@ -16,8 +11,7 @@ PyScene::PyScene(GameEngine* g) : Scene(g)
|
|||
registerAction(ActionCode::MOUSEWHEEL + ActionCode::WHEEL_DEL, "wheel_up");
|
||||
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()
|
||||
|
|
@ -26,42 +20,38 @@ void PyScene::update()
|
|||
|
||||
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());
|
||||
// Convert window coordinates to game coordinates using the viewport
|
||||
mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos));
|
||||
}
|
||||
|
||||
// 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))) {
|
||||
auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow());
|
||||
auto mousepos = game->getWindow().mapPixelToCoords(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;
|
||||
}
|
||||
*/
|
||||
target->click_callable->call(mousepos, button, type);
|
||||
return; // Stop after first handler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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()
|
||||
{
|
||||
// #118: Skip rendering if scene is not visible
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
game->getRenderTarget().clear();
|
||||
|
||||
// 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)
|
||||
game->getWindow().clear();
|
||||
|
||||
auto vec = *ui_elements;
|
||||
for (auto e: vec)
|
||||
{
|
||||
if (e) {
|
||||
// Track metrics
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (e)
|
||||
e->render();
|
||||
}
|
||||
|
||||
// Display is handled by GameEngine
|
||||
|
||||
game->getWindow().display();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,4 @@ public:
|
|||
void render() override final;
|
||||
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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__,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,61 +1,23 @@
|
|||
#include "PyTexture.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.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();
|
||||
if (!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
|
||||
texture.loadFromFile(source);
|
||||
auto size = texture.getSize();
|
||||
sheet_width = (size.x / sprite_width);
|
||||
sheet_height = (size.y / sprite_height);
|
||||
if (size.x % sprite_width != 0 || size.y % sprite_height != 0)
|
||||
{
|
||||
std::cout << "Warning: Texture `" << source << "` is not an even number of sprite widths or heights across." << std::endl
|
||||
std::cout << "Warning: Texture `" << source << "` is not an even number of sprite widths or heights across." << std::endl
|
||||
<< "Sprite size given was " << sprite_w << "x" << sprite_h << "px but the file has a resolution of " << sheet_width << "x" << sheet_height << "px." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// #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)
|
||||
{
|
||||
// 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;
|
||||
auto ir = sf::IntRect(tx * sprite_width, ty * sprite_height, sprite_width, sprite_height);
|
||||
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()
|
||||
{
|
||||
std::cout << "Find type" << std::endl;
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture");
|
||||
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;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii", const_cast<char**>(keywords), &filename, &sprite_width, &sprite_height))
|
||||
return -1;
|
||||
|
||||
// Create the texture object
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -126,49 +80,3 @@ PyObject* PyTexture::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
|||
{
|
||||
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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,39 +15,20 @@ private:
|
|||
sf::Texture texture;
|
||||
std::string source;
|
||||
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:
|
||||
int sprite_width, sprite_height; // just use them read only, OK?
|
||||
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));
|
||||
int getSpriteCount() const { return sheet_width * sheet_height; }
|
||||
|
||||
PyObject* pyObject();
|
||||
static PyObject* repr(PyObject*);
|
||||
static Py_hash_t hash(PyObject*);
|
||||
static int init(PyTextureObject*, PyObject*, PyObject*);
|
||||
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 {
|
||||
static PyTypeObject PyTextureType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Texture",
|
||||
.tp_basicsize = sizeof(PyTextureObject),
|
||||
.tp_itemsize = 0,
|
||||
|
|
@ -55,7 +36,6 @@ namespace mcrfpydef {
|
|||
.tp_hash = PyTexture::hash,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("SFML Texture Object"),
|
||||
.tp_getset = PyTexture::getsetters,
|
||||
//.tp_base = &PyBaseObject_Type,
|
||||
.tp_init = (initproc)PyTexture::init,
|
||||
.tp_new = PyType_GenericNew, //PyTexture::pynew,
|
||||
|
|
|
|||
357
src/PyTimer.cpp
357
src/PyTimer.cpp
|
|
@ -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}
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
533
src/PyVector.cpp
533
src/PyVector.cpp
|
|
@ -1,147 +1,21 @@
|
|||
#include "PyVector.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "PyRAII.h"
|
||||
#include <cmath>
|
||||
|
||||
PyGetSetDef PyVector::getsetters[] = {
|
||||
{"x", (getter)PyVector::get_member, (setter)PyVector::set_member,
|
||||
MCRF_PROPERTY(x, "X coordinate of the vector (float)"), (void*)0},
|
||||
{"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},
|
||||
{"x", (getter)PyVector::get_member, (setter)PyVector::set_member, "X/horizontal component", (void*)0},
|
||||
{"y", (getter)PyVector::get_member, (setter)PyVector::set_member, "Y/vertical component", (void*)1},
|
||||
{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)
|
||||
:data(target) {}
|
||||
|
||||
PyObject* PyVector::pyObject()
|
||||
{
|
||||
PyTypeObject* type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (!type) return nullptr;
|
||||
|
||||
PyVectorObject* obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
||||
Py_DECREF(type);
|
||||
|
||||
if (obj) {
|
||||
obj->data = data;
|
||||
}
|
||||
return (PyObject*)obj;
|
||||
PyObject* obj = PyType_GenericAlloc(&mcrfpydef::PyVectorType, 0);
|
||||
Py_INCREF(obj);
|
||||
PyVectorObject* self = (PyVectorObject*)obj;
|
||||
self->data = data;
|
||||
return obj;
|
||||
}
|
||||
|
||||
sf::Vector2f PyVector::fromPy(PyObject* obj)
|
||||
|
|
@ -226,398 +100,25 @@ PyObject* PyVector::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
|||
|
||||
PyObject* PyVector::get_member(PyObject* obj, void* closure)
|
||||
{
|
||||
PyVectorObject* self = (PyVectorObject*)obj;
|
||||
if (reinterpret_cast<long>(closure) == 0) {
|
||||
// x
|
||||
return PyFloat_FromDouble(self->data.x);
|
||||
} else {
|
||||
// y
|
||||
return PyFloat_FromDouble(self->data.y);
|
||||
}
|
||||
// TODO
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
int PyVector::set_member(PyObject* obj, PyObject* value, void* closure)
|
||||
{
|
||||
PyVectorObject* self = (PyVectorObject*)obj;
|
||||
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;
|
||||
}
|
||||
// TODO
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyVectorObject* PyVector::from_arg(PyObject* args)
|
||||
{
|
||||
// Use RAII for type reference management
|
||||
PyRAII::PyTypeRef type("Vector", McRFPy_API::mcrf_module);
|
||||
if (!type) {
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (PyObject_IsInstance(args, (PyObject*)type)) return (PyVectorObject*)args;
|
||||
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
||||
int err = init(obj, args, NULL);
|
||||
if (err) {
|
||||
Py_DECREF(obj);
|
||||
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);
|
||||
return obj;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,57 +25,18 @@ public:
|
|||
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 PyMethodDef methods[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
// Forward declare the PyNumberMethods and PySequenceMethods structures
|
||||
extern PyNumberMethods PyVector_as_number;
|
||||
extern PySequenceMethods PyVector_as_sequence;
|
||||
|
||||
static PyTypeObject PyVectorType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Vector",
|
||||
.tp_basicsize = sizeof(PyVectorObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_repr = PyVector::repr,
|
||||
.tp_as_number = &PyVector_as_number,
|
||||
.tp_as_sequence = &PyVector_as_sequence,
|
||||
.tp_hash = PyVector::hash,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("SFML Vector Object"),
|
||||
.tp_richcompare = PyVector::richcompare,
|
||||
.tp_methods = PyVector::methods,
|
||||
.tp_getset = PyVector::getsetters,
|
||||
.tp_init = (initproc)PyVector::init,
|
||||
.tp_new = PyVector::pynew,
|
||||
|
|
|
|||
532
src/PyWindow.cpp
532
src/PyWindow.cpp
|
|
@ -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}
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
|
@ -30,6 +30,16 @@ std::string Scene::action(int 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)
|
||||
{
|
||||
|
|
@ -54,43 +64,3 @@ void Scene::key_unregister()
|
|||
*/
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
17
src/Scene.h
17
src/Scene.h
|
|
@ -4,6 +4,7 @@
|
|||
#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 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 <list>
|
||||
|
|
@ -35,22 +36,14 @@ public:
|
|||
bool hasAction(std::string);
|
||||
bool hasAction(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;
|
||||
|
||||
//PyObject* key_callable;
|
||||
std::unique_ptr<PyKeyCallable> key_callable;
|
||||
void key_register(PyObject*);
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
136
src/Timer.cpp
136
src/Timer.cpp
|
|
@ -1,147 +1,31 @@
|
|||
#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)
|
||||
: callback(std::make_shared<PyCallable>(_target)), interval(_interval), last_ran(now),
|
||||
paused(false), pause_start_time(0), total_paused_time(0), once(_once)
|
||||
Timer::Timer(PyObject* _target, int _interval, int now)
|
||||
: target(_target), interval(_interval), last_ran(now)
|
||||
{}
|
||||
|
||||
Timer::Timer()
|
||||
: callback(std::make_shared<PyCallable>(Py_None)), interval(0), last_ran(0),
|
||||
paused(false), pause_start_time(0), total_paused_time(0), once(false)
|
||||
: target(Py_None), interval(0), last_ran(0)
|
||||
{}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!callback || callback->isNone()) return false;
|
||||
|
||||
if (hasElapsed(now))
|
||||
if (!target || target == Py_None) return false;
|
||||
if (now > last_ran + interval)
|
||||
{
|
||||
last_ran = now;
|
||||
|
||||
// Get the PyTimer wrapper from cache to pass to callback
|
||||
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);
|
||||
|
||||
PyObject* args = Py_BuildValue("(i)", now);
|
||||
PyObject* retval = PyObject_Call(target, args, NULL);
|
||||
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_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)
|
||||
{
|
||||
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);
|
||||
std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
}
|
||||
|
||||
// Handle one-shot timers
|
||||
if (once) {
|
||||
cancel();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
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
Loading…
Reference in New Issue