Compare commits

..

2 Commits

Author SHA1 Message Date
John McCardle bd6407db29 hotfix: bad documentation links... ...because of trailing slash?! 2025-07-17 23:49:03 -04:00
John McCardle f4343e1e82 Squashed commit of the following: [alpha_presentable]
Author: John McCardle <mccardle.john@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>

commit dc47f2474c7b2642d368f9772894aed857527807
    the UIEntity rant

commit 673ca8e1b089ea670257fc04ae1a676ed95a40ed
    I forget when these tests were written, but I want them in the squash merge

commit 70c71565c684fa96e222179271ecb13a156d80ad
    Fix UI object segfault by switching from managed to manual weakref management

    The UI types (Frame, Caption, Sprite, Grid, Entity) were using
    Py_TPFLAGS_MANAGED_WEAKREF while also trying to manually create weakrefs
    for the PythonObjectCache. This is fundamentally incompatible - when
    Python manages weakrefs internally, PyWeakref_NewRef() cannot access the
    weakref list properly, causing segfaults.

    Changed all UI types to use manual weakref management (like PyTimer):
    - Restored weakreflist field in all UI type structures
    - Removed Py_TPFLAGS_MANAGED_WEAKREF from all UI type flags
    - Added tp_weaklistoffset for all UI types in module initialization
    - Initialize weakreflist=NULL in tp_new and init methods
    - Call PyObject_ClearWeakRefs() in dealloc functions

    This allows the PythonObjectCache to continue working correctly,
    maintaining Python object identity for C++ objects across the boundary.

    Fixes segfault when creating UI objects (e.g., Caption, Grid) that was
    preventing tutorial scripts from running.

This is the bulk of the required behavior for Issue #126.
that issure isn't ready for closure yet; several other sub-issues left.
    closes #110
    mention issue #109 - resolves some __init__ related nuisances

commit 3dce3ec539ae99e32d869007bf3f49d03e4e2f89
    Refactor timer system for cleaner architecture and enhanced functionality

    Major improvements to the timer system:
    - Unified all timer logic in the Timer class (C++)
    - Removed PyTimerCallable subclass, now using PyCallable directly
    - Timer objects are now passed to callbacks as first argument
    - Added 'once' parameter for one-shot timers that auto-stop
    - Implemented proper PythonObjectCache integration with weakref support

    API enhancements:
    - New callback signature: callback(timer, runtime) instead of just (runtime)
    - Timer objects expose: name, interval, remaining, paused, active, once properties
    - Methods: pause(), resume(), cancel(), restart()
    - Comprehensive documentation with examples
    - Enhanced repr showing timer state (active/paused/once/remaining time)

    This cleanup follows the UIEntity/PyUIEntity pattern and makes the timer
    system more Pythonic while maintaining backward compatibility through
    the legacy setTimer/delTimer API.

    closes #121

commit 145834cfc31b8dabc4cb3591b9cb4ed99fc8b964
    Implement Python object cache to preserve derived types in collections

    Add a global cache system that maintains weak references to Python objects,
    ensuring that derived Python classes maintain their identity when stored in
    and retrieved from C++ collections.

    Key changes:
    - Add PythonObjectCache singleton with serial number system
    - Each cacheable object (UIDrawable, UIEntity, Timer, Animation) gets unique ID
    - Cache stores weak references to prevent circular reference memory leaks
    - Update all UI type definitions to support weak references (Py_TPFLAGS_MANAGED_WEAKREF)
    - Enable subclassing for all UI types (Py_TPFLAGS_BASETYPE)
    - Collections check cache before creating new Python wrappers
    - Register objects in cache during __init__ methods
    - Clean up cache entries in C++ destructors

    This ensures that Python code like:
    ```python
    class MyFrame(mcrfpy.Frame):
        def __init__(self):
            super().__init__()
            self.custom_data = "preserved"

    frame = MyFrame()
    scene.ui.append(frame)
    retrieved = scene.ui[0]  # Same MyFrame instance with custom_data intact
    ```

    Works correctly, with retrieved maintaining the derived type and custom attributes.

    Closes #112

commit c5e7e8e298
    Update test demos for new Python API and entity system

    - Update all text input demos to use new Entity constructor signature
    - Fix pathfinding showcase to work with new entity position handling
    - Remove entity_waypoints tracking in favor of simplified movement
    - Delete obsolete exhaustive_api_demo.py (superseded by newer demos)
    - Adjust entity creation calls to match Entity((x, y), texture, sprite_index) pattern

commit 6d29652ae7
    Update animation demo suite with crash fixes and improvements

    - Add warnings about AnimationManager segfault bug in sizzle_reel_final.py
    - Create sizzle_reel_final_fixed.py that works around the crash by hiding objects instead of removing them
    - Increase font sizes for better visibility in demos
    - Extend demo durations for better showcase of animations
    - Remove debug prints from animation_sizzle_reel_working.py
    - Minor cleanup and improvements to all animation demos

commit a010e5fa96
    Update game scripts for new Python API

    - Convert entity position access from tuple to x/y properties
    - Update caption size property to font_size
    - Fix grid boundary checks to use grid_size instead of exceptions
    - Clean up demo timer on menu exit to prevent callbacks

    These changes adapt the game scripts to work with the new standardized
    Python API constructors and property names.

commit 9c8d6c4591
    Fix click event z-order handling in PyScene

    Changed click detection to properly respect z-index by:
    - Sorting ui_elements in-place when needed (same as render order)
    - Using reverse iterators to check highest z-index elements first
    - This ensures top-most elements receive clicks before lower ones

commit dcd1b0ca33
    Add roguelike tutorial implementation files

    Implement Parts 0-2 of the classic roguelike tutorial adapted for McRogueFace:
    - Part 0: Basic grid setup and tile rendering
    - Part 1: Drawing '@' symbol and basic movement
    - Part 1b: Variant with sprite-based player
    - Part 2: Entity system and NPC implementation with three movement variants:
      - part_2.py: Standard implementation
      - part_2-naive.py: Naive movement approach
      - part_2-onemovequeued.py: Queued movement system

    Includes tutorial assets:
    - tutorial2.png: Tileset for dungeon tiles
    - tutorial_hero.png: Player sprite sheet

commit 6813fb5129
    Standardize Python API constructors and remove PyArgHelpers

    - Remove PyArgHelpers.h and all macro-based argument parsing
    - Convert all UI class constructors to use PyArg_ParseTupleAndKeywords
    - Standardize constructor signatures across UICaption, UIEntity, UIFrame, UIGrid, and UISprite
    - Replace PYARGHELPER_SINGLE/MULTI macros with explicit argument parsing
    - Improve error messages and argument validation
    - Maintain backward compatibility with existing Python code

    This change improves code maintainability and consistency across the Python API.

commit 6f67fbb51e
    Fix animation callback crashes from iterator invalidation (#119)

    Resolved segfaults caused by creating new animations from within
    animation callbacks. The issue was iterator invalidation in
    AnimationManager::update() when callbacks modified the active
    animations vector.

    Changes:
    - Add deferred animation queue to AnimationManager
    - New animations created during update are queued and added after
    - Set isUpdating flag to track when in update loop
    - Properly handle Animation destructor during callback execution
    - Add clearCallback() method for safe cleanup scenarios

    This fixes the "free(): invalid pointer" and "malloc(): unaligned
    fastbin chunk detected" errors that occurred with rapid animation
    creation in callbacks.

commit eb88c7b3aa
    Add animation completion callbacks (#119)

    Implement callbacks that fire when animations complete, enabling direct
    causality between animation end and game state changes. This eliminates
    race conditions from parallel timer workarounds.

    - Add optional callback parameter to Animation constructor
    - Callbacks execute synchronously when animation completes
    - Proper Python reference counting with GIL safety
    - Callbacks receive (anim, target) parameters (currently None)
    - Exception handling prevents crashes from Python errors

    Example usage:
    ```python
    def on_complete(anim, target):
        player_moving = False

    anim = mcrfpy.Animation("x", 300.0, 1.0, "easeOut", callback=on_complete)
    anim.start(player)
    ```

    closes #119

commit 9fb428dd01
    Update ROADMAP with GitHub issue numbers (#111-#125)

    Added issue numbers from GitHub tracker to roadmap items:
    - #111: Grid Click Events Broken in Headless
    - #112: Object Splitting Bug (Python type preservation)
    - #113: Batch Operations for Grid
    - #114: CellView API
    - #115: SpatialHash Implementation
    - #116: Dirty Flag System
    - #117: Memory Pool for Entities
    - #118: Scene as Drawable
    - #119: Animation Completion Callbacks
    - #120: Animation Property Locking
    - #121: Timer Object System
    - #122: Parent-Child UI System
    - #123: Grid Subgrid System
    - #124: Grid Point Animation
    - #125: GitHub Issues Automation

    Also updated existing references:
    - #101/#110: Constructor standardization
    - #109: Vector class indexing

    Note: Tutorial-specific items and Python-implementable features
    (input queue, collision reservation) are not tracked as engine issues.

commit 062e4dadc4
    Fix animation segfaults with RAII weak_ptr implementation

    Resolved two critical segmentation faults in AnimationManager:
    1. Race condition when creating multiple animations in timer callbacks
    2. Exit crash when animations outlive their target objects

    Changes:
    - Replace raw pointers with std::weak_ptr for automatic target invalidation
    - Add Animation::complete() to jump animations to final value
    - Add Animation::hasValidTarget() to check if target still exists
    - Update AnimationManager to auto-remove invalid animations
    - Add AnimationManager::clear() call to GameEngine::cleanup()
    - Update Python bindings to pass shared_ptr instead of raw pointers

    This ensures animations can never reference destroyed objects, following
    proper RAII principles. Tested with sizzle_reel_final.py and stress
    tests creating/destroying hundreds of animated objects.

commit 98fc49a978
    Directory structure cleanup and organization overhaul
2025-07-15 21:30:49 -04:00
36 changed files with 1876 additions and 1646 deletions

View File

@ -64,11 +64,11 @@ For comprehensive documentation, tutorials, and API reference, visit:
The documentation site includes: The documentation site includes:
- **[Quickstart Guide](https://mcrogueface.github.io/quickstart/)** - Get running in 5 minutes - **[Quickstart Guide](https://mcrogueface.github.io/quickstart)** - Get running in 5 minutes
- **[McRogueFace Does The Entire Roguelike Tutorial](https://mcrogueface.github.io/tutorials/)** - Step-by-step game building - **[McRogueFace Does The Entire Roguelike Tutorial](https://mcrogueface.github.io/tutorials)** - Step-by-step game building
- **[Complete API Reference](https://mcrogueface.github.io/api/)** - Every function documented - **[Complete API Reference](https://mcrogueface.github.io/api)** - Every function documented
- **[Cookbook](https://mcrogueface.github.io/cookbook/)** - Ready-to-use code recipes - **[Cookbook](https://mcrogueface.github.io/cookbook)** - Ready-to-use code recipes
- **[C++ Extension Guide](https://mcrogueface.github.io/extending-cpp/)** - For C++ developers: Add engine features - **[C++ Extension Guide](https://mcrogueface.github.io/extending-cpp)** - For C++ developers: Add engine features
## Build Requirements ## Build Requirements

View File

@ -1,935 +0,0 @@
# McRogueFace - Development Roadmap
## 🚨 URGENT PRIORITIES - July 12, 2025 🚨
### CRITICAL: RoguelikeDev Tutorial Event starts July 15! (3 days)
#### 1. Tutorial Status & Blockers
- [x] **Part 0**: Complete (Starting McRogueFace)
- [x] **Part 1**: Complete (Setting up grid and tile sheet)
- [ ] **Part 2**: Draft exists but BLOCKED by animation issues - PRIORITY FIX!
- [ ] **Parts 3-6**: Machine-generated drafts need complete rework
- [ ] **Parts 7-15**: Need creation this weekend
**Key Blockers**:
- Need smooth character movement animation (Pokemon-style)
- Grid needs walkable grass center, non-walkable tree edges
- Input queueing during animations not working properly
#### 2. Animation System Critical Issues 🚨
**BLOCKER FOR TUTORIAL PART 2**:
- [ ] **Input Queue System**: Holding arrow keys doesn't queue movements
- Animation must complete before next input accepted
- Need "press and hold" that queues ONE additional move
- Goal: Pokemon-style smooth continuous movement
- [ ] **Collision Reservation**: When entity starts moving, should block destination
- Prevents overlapping movements
- Already claimed tiles should reject incoming entities
- [x] **Segfault Fix**: Refactored from bare pointers to weak references ✅
#### 3. Grid Clicking BROKEN in Headless Mode 🚨
**MAJOR DISCOVERY**: All click events commented out!
- [ ] **#111** - Grid Click Events Broken in Headless: All click events commented out
- [ ] **Grid Click Coordinates**: Need tile coords, not just mouse coords
- [ ] **Nested Grid Support**: Clicks must work on grids within frames
- [ ] **No Error Reporting**: System claimed complete but isn't
#### 4. Python API Consistency Crisis
**Tutorial Writing Reveals Major Issues**:
- [ ] **#101/#110** - Inconsistent Constructors: Each class has different requirements
- [ ] **#109** - Vector Class Broken: No [0], [1] indexing like tuples
- [ ] **#112** - Object Splitting Bug: Python derived classes lose type in collections
- Shared pointer extracted, Python reference discarded
- Retrieved objects are base class only
- No way to cast back to derived type
- [ ] **Need Systematic Generation**: All bindings should be consistent
- [x] **UIGrid TCOD Integration** (8 hours) ✅ COMPLETED!
- ✅ Add TCODMap* to UIGrid constructor with proper lifecycle
- ✅ Implement complete Dijkstra pathfinding system
- ✅ Create mcrfpy.libtcod submodule with Python bindings
- ✅ Fix critical PyArg bug preventing Color object assignments
- ✅ Implement FOV with perspective rendering
- [ ] **#113** - Add batch operations for NumPy-style access (deferred)
- [ ] **#114** - Create CellView for ergonomic .at((x,y)) access (deferred)
- [x] **UIEntity Pathfinding** (4 hours) ✅ COMPLETED!
- ✅ Implement Dijkstra maps for multiple targets in UIGrid
- ✅ Add path_to(target) method using A* to UIEntity
- ✅ Cache paths in UIEntity for performance
#### 3. Performance Critical Path
- [ ] **#115** - Implement SpatialHash for 10,000+ entities (2 hours)
- [ ] **#116** - Add dirty flag system to UIGrid (1 hour)
- [ ] **#113** - Batch update context managers (2 hours)
- [ ] **#117** - Memory pool for entities (2 hours)
#### 4. Bug Fixing Pipeline
- [ ] **#125** - Set up GitHub Issues automation
- [ ] Create test for each bug before fixing
- [ ] Track: Memory leaks, Segfaults, Python/C++ boundary errors
---
## 🏗️ PROPOSED ARCHITECTURE IMPROVEMENTS (From July 12 Analysis)
### Object-Oriented Design Overhaul
1. **Scene System Revolution**:
- [ ] **#118** - Make Scene derive from Drawable (scenes are drawn!)
- [ ] Give scenes position and visibility properties
- [ ] Scene selection by visibility (auto-hide old scene)
- [ ] Replace transition system with animations
2. **Animation System Enhancements**:
- [ ] **#119** - Add proper completion callbacks (object + animation params)
- [ ] **#120** - Prevent property conflicts (exclusive locking)
- [ ] Currently using timer sync workarounds
3. **Timer System Improvements**:
- [ ] **#121** - Replace string-dictionary system with objects
- [ ] Add start(), stop(), pause() methods
- [ ] Implement proper one-shot mode
- [ ] Pass timer object to callbacks (not just ms)
4. **Parent-Child UI Relationships**:
- [ ] **#122** - Add parent field to UI drawables (like entities have)
- [ ] Implement append/remove/extend with auto-parent updates
- [ ] Auto-remove from old parent when adding to new
### Performance Optimizations Needed
- [ ] **Grid Rendering**: Consider texture caching vs real-time
- [ ] **#123** - Subgrid System: Split large grids into 256x256 chunks
- [ ] **#116** - Dirty Flagging: Propagate from base class up
- [ ] **#124** - Animation Features: Tile color animation, sprite cycling
---
## ⚠️ CLAUDE CODE QUALITY CONCERNS (6-7 Weeks In)
### Issues Observed:
1. **Declining Quality**: High quantity but low quality results
2. **Not Following Requirements**: Ignoring specific implementation needs
3. **Bad Practices**:
- Creating parallel copies (animation_RAII.cpp, _fixed, _final versions)
- Should use Git, not file copies
- Claims functionality "done" when stubbed out
4. **File Management Problems**:
- Git operations reset timestamps
- Can't determine creation order of multiple versions
### Recommendations:
- Use Git for version control exclusively
- Fix things in place, not copies
- Acknowledge incomplete functionality
- Follow project's implementation style
---
## 🎯 STRATEGIC ARCHITECTURE VISION
### Three-Layer Grid Architecture (From Compass Research)
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 1000x1000 maps)
- **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
---
## Project Status: 🎉 ALPHA 0.1 RELEASE! 🎉
**Current State**: Documentation system complete, TCOD integration urgent
**Latest Update**: Tutorial Parts 0-6 complete with documentation (2025-07-11)
**Branch**: alpha_streamline_2
**Open Issues**: ~46 remaining + URGENT TCOD/Tutorial work
---
## 📋 TCOD Integration Implementation Details
### Phase 1: Core UIGrid Integration (Day 1 Morning)
```cpp
// UIGrid.h additions
class UIGrid : public UIDrawable {
private:
TCODMap* world_state; // Add TCOD map
std::unordered_map<int, UIGridPointState*> entity_perspectives;
bool batch_mode = false;
std::vector<CellUpdate> pending_updates;
```
### Phase 2: Python Bindings (Day 1 Afternoon)
```python
# New API surface
grid = mcrfpy.Grid(100, 100)
grid.compute_fov(player.x, player.y, radius=10) # Returns visible cells
grid.at((x, y)).walkable = False # Ergonomic access
with grid.batch_update(): # Context manager for performance
# All updates batched
```
### Phase 3: Entity Integration (Day 2 Morning)
```python
# UIEntity additions
entity.path_to(target_x, target_y) # A* pathfinding
entity.flee_from(threat) # Dijkstra map
entity.can_see(other_entity) # FOV check
```
### Critical Success Factors:
1. **Batch everything** - Never update single cells in loops
2. **Lazy evaluation** - Only compute FOV for entities that need it
3. **Sparse storage** - Don't store full grids per entity
4. **Profile early** - Find the 20% of code taking 80% of time
---
## Recent Achievements
### 2025-07-12: Animation System RAII Overhaul - Critical Segfault Fix! 🛡️
**Fixed two major crashes in AnimationManager**
- ✅ Race condition when creating animations in timer callbacks
- ✅ Exit crash when animations outlive their targets
- ✅ Implemented weak_ptr tracking for automatic cleanup
- ✅ Added complete() and hasValidTarget() methods
- ✅ No more use-after-free bugs - proper RAII design
- ✅ Extensively tested with stress tests and production demos
### 2025-07-10: Complete FOV, A* Pathfinding & GUI Text Widgets! 👁️🗺️⌨️
**Engine Feature Sprint - Major Capabilities Added**
- ✅ Complete FOV (Field of View) system with perspective rendering
- UIGrid.perspective property controls which entity's view to render
- Three-layer overlay system: unexplored (black), explored (dark), visible (normal)
- Per-entity visibility state tracking with UIGridPointState
- Perfect knowledge updates - only explored areas persist
- ✅ A* Pathfinding implementation
- Entity.path_to(x, y) method for direct pathfinding
- UIGrid compute_astar() and get_astar_path() methods
- Path caching in entities for performance
- Complete test suite comparing A* vs Dijkstra performance
- ✅ GUI Text Input Widget System
- Full-featured TextInputWidget class with cursor, selection, scrolling
- Improved widget with proper text rendering and multi-line support
- Example showcase demonstrating multiple input fields
- Foundation for in-game consoles, chat systems, and text entry
- ✅ Sizzle Reel Demos
- path_vision_sizzle_reel.py combines pathfinding with FOV
- Interactive visibility demos showing real-time FOV updates
- Performance demonstrations with multiple entities
### 2025-07-09: Dijkstra Pathfinding & Critical Bug Fix! 🗺️
**TCOD Integration Sprint - Major Progress**
- ✅ Complete Dijkstra pathfinding implementation in UIGrid
- compute_dijkstra(), get_dijkstra_distance(), get_dijkstra_path() methods
- Full TCODMap and TCODDijkstra integration with proper memory management
- Comprehensive test suite with both headless and interactive demos
- ✅ **CRITICAL FIX**: PyArg bug in UIGridPoint color setter
- Now supports both mcrfpy.Color objects and (r,g,b,a) tuples
- Eliminated mysterious "SystemError: new style getargs format" crashes
- Proper error handling and exception propagation
- ✅ mcrfpy.libtcod submodule with Python bindings
- dijkstra_compute(), dijkstra_get_distance(), dijkstra_get_path()
- line() function for corridor generation
- Foundation ready for FOV implementation
- ✅ Test consolidation: 6 broken demos → 2 clean, working versions
### 2025-07-08: PyArgHelpers Infrastructure Complete! 🔧
**Standardized Python API Argument Parsing**
- Unified position handling: (x, y) tuples or separate x, y args
- Consistent size parsing: (w, h) tuples or width, height args
- Grid-specific helpers for tile-based positioning
- Proper conflict detection between positional and keyword args
- All UI components migrated: Frame, Caption, Sprite, Grid, Entity
- Improved error messages: "Value must be a number (int or float)"
- Foundation for Phase 7 documentation efforts
### 2025-07-05: ALPHA 0.1 ACHIEVED! 🎊🍾
**All Alpha Blockers Resolved!**
- Z-order rendering with performance optimization (Issue #63)
- Python Sequence Protocol for collections (Issue #69)
- Comprehensive Animation System (Issue #59)
- Moved RenderTexture to Beta (not needed for Alpha)
- **McRogueFace is ready for Alpha release!**
### 2025-07-05: Z-order Rendering Complete! 🎉
**Issue #63 Resolved**: Consistent z-order rendering with performance optimization
- Dirty flag pattern prevents unnecessary per-frame sorting
- Lazy sorting for both Scene elements and Frame children
- Frame children now respect z_index (fixed inconsistency)
- Automatic dirty marking on z_index changes and collection modifications
- Performance: O(1) check for static scenes vs O(n log n) every frame
### 2025-07-05: Python Sequence Protocol Complete! 🎉
**Issue #69 Resolved**: Full sequence protocol implementation for collections
- Complete __setitem__, __delitem__, __contains__ support
- Slice operations with extended slice support (step != 1)
- Concatenation (+) and in-place concatenation (+=) with validation
- Negative indexing throughout, index() and count() methods
- Type safety: UICollection (Frame/Caption/Sprite/Grid), EntityCollection (Entity only)
- Default value support: None for texture/font parameters uses engine defaults
### 2025-07-05: Animation System Complete! 🎉
**Issue #59 Resolved**: Comprehensive animation system with 30+ easing functions
- Property-based animations for all UI classes (Frame, Caption, Sprite, Grid, Entity)
- Individual color component animation (r/g/b/a)
- Sprite sequence animation and text typewriter effects
- Pure C++ execution without Python callbacks
- Delta animation support for relative values
### 2025-01-03: Major Stability Update
**Major Cleanup**: Removed deprecated registerPyAction system (-180 lines)
**Bug Fixes**: 12 critical issues including Grid segfault, Issue #78 (middle click), Entity setters
**New Features**: Entity.index() (#73), EntityCollection.extend() (#27), Sprite validation (#33)
**Test Coverage**: Comprehensive test suite with timer callback pattern established
---
## 🔧 CURRENT WORK: Alpha Streamline 2 - Major Architecture Improvements
### Recent Completions:
- ✅ **Phase 1-4 Complete** - Foundation, API Polish, Entity Lifecycle, Visibility/Performance
- ✅ **Phase 5 Complete** - Window/Scene Architecture fully implemented!
- Window singleton with properties (#34)
- OOP Scene support with lifecycle methods (#61)
- Window resize events (#1)
- Scene transitions with animations (#105)
- ✅ **Phase 6 Complete** - Rendering Revolution achieved!
- Grid background colors (#50) ✅
- RenderTexture overhaul (#6) ✅
- UIFrame clipping support ✅
- Viewport-based rendering (#8) ✅
### Active Development:
- **Branch**: alpha_streamline_2
- **Current Phase**: Phase 7 - Documentation & Distribution
- **Achievement**: PyArgHelpers infrastructure complete - standardized Python API
- **Strategic Vision**: See STRATEGIC_VISION.md for platform roadmap
- **Latest**: All UI components now use consistent argument parsing patterns!
### 🏗️ Architectural Dependencies Map
```
Foundation Layer:
├── #71 Base Class (_Drawable)
│ ├── #10 Visibility System (needs AABB from base)
│ ├── #87 visible property
│ └── #88 opacity property
├── #7 Safe Constructors (affects all classes)
│ └── Blocks any new class creation until resolved
└── #30 Entity/Grid Integration (lifecycle management)
└── Enables reliable entity management
Window/Scene Layer:
├── #34 Window Object
│ ├── #61 Scene Object (depends on Window)
│ ├── #14 SFML Exposure (helps implement Window)
│ └── Future: Multi-window support
Rendering Layer:
└── #6 RenderTexture Overhaul
├── Enables clipping
├── Off-screen rendering
└── Post-processing effects
```
## 🚀 Alpha Streamline 2 - Comprehensive Phase Plan
### Phase 1: Foundation Stabilization (1-2 weeks)
**Goal**: Safe, predictable base for all future work
```
1. #7 - Audit and fix unsafe constructors (CRITICAL - do first!)
- Find all manually implemented no-arg constructors
- Verify map compatibility requirements
- Make pointer-safe or remove
2. #71 - _Drawable base class implementation
- Common properties: x, y, w, h, visible, opacity
- Virtual methods: get_bounds(), render()
- Proper Python inheritance setup
3. #87 - visible property
- Add to base class
- Update all render methods to check
4. #88 - opacity property (depends on #87)
- 0.0-1.0 float range
- Apply in render methods
5. #89 - get_bounds() method
- Virtual method returning (x, y, w, h)
- Override in each UI class
6. #98 - move()/resize() convenience methods
- move(dx, dy) - relative movement
- resize(w, h) - absolute sizing
```
*Rationale*: Can't build on unsafe foundations. Base class enables all UI improvements.
### Phase 2: Constructor & API Polish (1 week)
**Goal**: Pythonic, intuitive API
```
1. #101 - Standardize (0,0) defaults for all positions
2. #38 - Frame children parameter: Frame(children=[...])
3. #42 - Click handler in __init__: Button(click=callback)
4. #90 - Grid size tuple: Grid(grid_size=(10, 10))
5. #19 - Sprite texture swapping: sprite.texture = new_texture
6. #52 - Grid skip out-of-bounds entities (performance)
```
*Rationale*: Quick wins that make the API more pleasant before bigger changes.
### Phase 3: Entity Lifecycle Management (1 week)
**Goal**: Bulletproof entity/grid relationships
```
1. #30 - Entity.die() and grid association
- Grid.entities.append(e) sets e.grid = self
- Grid.entities.remove(e) sets e.grid = None
- Entity.die() calls self.grid.remove(self)
- Entity can only be in 0 or 1 grid
2. #93 - Vector arithmetic methods
- add, subtract, multiply, divide
- distance, normalize, dot product
3. #94 - Color helper methods
- from_hex("#FF0000"), to_hex()
- lerp(other_color, t) for interpolation
4. #103 - Timer objects
timer = mcrfpy.Timer("my_timer", callback, 1000)
timer.pause()
timer.resume()
timer.cancel()
```
*Rationale*: Games need reliable entity management. Timer objects enable entity AI.
### Phase 4: Visibility & Performance (1-2 weeks)
**Goal**: Only render/process what's needed
```
1. #10 - [UNSCHEDULED] Full visibility system with AABB
- Postponed: UIDrawables can exist in multiple collections
- Cannot reliably determine screen position due to multiple render contexts
- Needs architectural solution for parent-child relationships
2. #52 - Grid culling (COMPLETED in Phase 2)
3. #39/40/41 - Name system for finding elements
- name="button1" property on all UIDrawables
- only_one=True for unique names
- scene.find("button1") returns element
- collection.find("enemy*") returns list
4. #104 - Basic profiling/metrics
- Frame time tracking
- Draw call counting
- Python vs C++ time split
```
*Rationale*: Performance is feature. Finding elements by name is huge QoL.
### Phase 5: Window/Scene Architecture ✅ COMPLETE! (2025-07-06)
**Goal**: Modern, flexible architecture
```
1. ✅ #34 - Window object (singleton first)
window = mcrfpy.Window.get()
window.resolution = (1920, 1080)
window.fullscreen = True
window.vsync = True
2. ✅ #1 - Window resize events
scene.on_resize(self, width, height) callback implemented
3. ✅ #61 - Scene object (OOP scenes)
class MenuScene(mcrfpy.Scene):
def on_keypress(self, key, state):
# handle input
def on_enter(self):
# setup UI
def on_exit(self):
# cleanup
def update(self, dt):
# frame update
4. ✅ #14 - SFML exposure research
- Completed comprehensive analysis
- Recommendation: Direct integration as mcrfpy.sfml
- SFML 3.0 migration deferred to late 2025
5. ✅ #105 - Scene transitions
mcrfpy.setScene("menu", "fade", 1.0)
# Supports: fade, slide_left, slide_right, slide_up, slide_down
```
*Result*: Entire window/scene system modernized with OOP design!
### Phase 6: Rendering Revolution (3-4 weeks) ✅ COMPLETE!
**Goal**: Professional rendering capabilities
```
1. ✅ #50 - Grid background colors [COMPLETED]
grid.background_color = mcrfpy.Color(50, 50, 50)
- Added background_color property with animation support
- Default dark gray background (8, 8, 8, 255)
2. ✅ #6 - RenderTexture overhaul [COMPLETED]
✅ Base infrastructure in UIDrawable
✅ UIFrame clip_children property
✅ Dirty flag optimization system
✅ Nested clipping support
✅ UIGrid already has appropriate RenderTexture implementation
❌ UICaption/UISprite clipping not needed (no children)
3. ✅ #8 - Viewport-based rendering [COMPLETED]
- Fixed game resolution (window.game_resolution)
- Three scaling modes: "center", "stretch", "fit"
- Window to game coordinate transformation
- Mouse input properly scaled with windowToGameCoords()
- Python API fully integrated
- Tests: test_viewport_simple.py, test_viewport_visual.py, test_viewport_scaling.py
4. #106 - Shader support [DEFERRED TO POST-PHASE 7]
sprite.shader = mcrfpy.Shader.load("glow.frag")
frame.shader_params = {"intensity": 0.5}
5. #107 - Particle system [DEFERRED TO POST-PHASE 7]
emitter = mcrfpy.ParticleEmitter()
emitter.texture = spark_texture
emitter.emission_rate = 100
emitter.lifetime = (0.5, 2.0)
```
**Phase 6 Achievement Summary**:
- Grid backgrounds (#50) ✅ - Customizable background colors with animation
- RenderTexture overhaul (#6) ✅ - UIFrame clipping with opt-in architecture
- Viewport rendering (#8) ✅ - Three scaling modes with coordinate transformation
- UIGrid already had optimal RenderTexture implementation for its use case
- UICaption/UISprite clipping unnecessary (no children to clip)
- Performance optimized with dirty flag system
- Backward compatibility preserved throughout
- Effects/Shader/Particle systems deferred for focused delivery
*Rationale*: This unlocks professional visual effects but is complex.
### Phase 7: Documentation & Distribution (1-2 weeks)
**Goal**: Ready for the world
```
1. ✅ #85 - Replace all "docstring" placeholders [COMPLETED 2025-07-08]
2. ✅ #86 - Add parameter documentation [COMPLETED 2025-07-08]
3. ✅ #108 - Generate .pyi type stubs for IDE support [COMPLETED 2025-07-08]
4. ❌ #70 - PyPI wheel preparation [CANCELLED - Architectural mismatch]
5. API reference generator tool
```
## 📋 Critical Path & Parallel Tracks
### 🔴 **Critical Path** (Must do in order)
**Safe Constructors (#7)** → **Base Class (#71)****Visibility (#10)****Window (#34)** → **Scene (#61)**
### 🟡 **Parallel Tracks** (Can be done alongside critical path)
**Track A: Entity Systems**
- Entity/Grid integration (#30)
- Timer objects (#103)
- Vector/Color helpers (#93, #94)
**Track B: API Polish**
- Constructor improvements (#101, #38, #42, #90)
- Sprite texture swap (#19)
- Name/search system (#39/40/41)
**Track C: Performance**
- Grid culling (#52)
- Visibility culling (part of #10)
- Profiling tools (#104)
### 💎 **Quick Wins to Sprinkle Throughout**
1. Color helpers (#94) - 1 hour
2. Vector methods (#93) - 1 hour
3. Grid backgrounds (#50) - 30 minutes
4. Default positions (#101) - 30 minutes
### 🎯 **Recommended Execution Order**
**Week 1-2**: Foundation (Critical constructors + base class)
**Week 3**: Entity lifecycle + API polish
**Week 4**: Visibility system + performance
**Week 5-6**: Window/Scene architecture
**Week 7-9**: Rendering revolution (or defer to gamma)
**Week 10**: Documentation + release prep
### 🆕 **New Issues to Create/Track**
1. [x] **Timer Objects** - Pythonic timer management (#103) - *Completed Phase 3*
2. [ ] **Event System Enhancement** - Mouse enter/leave, drag, right-click
3. [ ] **Resource Manager** - Centralized asset loading
4. [ ] **Serialization System** - Save/load game state
5. [x] **Scene Transitions** - Fade, slide, custom effects (#105) - *Completed Phase 5*
6. [x] **Profiling Tools** - Performance metrics (#104) - *Completed Phase 4*
7. [ ] **Particle System** - Visual effects framework (#107)
8. [ ] **Shader Support** - Custom rendering effects (#106)
---
## 📋 Phase 6 Implementation Strategy
### RenderTexture Overhaul (#6) - Technical Approach
**Current State**:
- UIGrid already uses RenderTexture for entity rendering
- Scene transitions use RenderTextures for smooth animations
- Direct rendering to window for Frame, Caption, Sprite
**Implementation Plan**:
1. **Base Infrastructure**:
- Add `sf::RenderTexture* target` to UIDrawable base
- Modify `render()` to check if target exists
- If target: render to texture, then draw texture to parent
- If no target: render directly (backward compatible)
2. **Clipping Support**:
- Frame enforces bounds on children via RenderTexture
- Children outside bounds are automatically clipped
- Nested frames create render texture hierarchy
3. **Performance Optimization**:
- Lazy RenderTexture creation (only when needed)
- Dirty flag system (only re-render when changed)
- Texture pooling for commonly used sizes
4. **Integration Points**:
- Scene transitions already working with RenderTextures
- UIGrid can be reference implementation
- Test with deeply nested UI structures
**Quick Wins Before Core Work**:
1. **Grid Background (#50)** - 30 min implementation
- Add `background_color` and `background_texture` properties
- Render before entities in UIGrid::render()
- Good warm-up before tackling RenderTexture
2. **Research Tasks**:
- Study UIGrid's current RenderTexture usage
- Profile scene transition performance
- Identify potential texture size limits
---
## 🚀 NEXT PHASE: Beta Features & Polish
### Alpha Complete! Moving to Beta Priorities:
1. ~~**#69** - Python Sequence Protocol for collections~~ - *Completed! (2025-07-05)*
2. ~~**#63** - Z-order rendering for UIDrawables~~ - *Completed! (2025-07-05)*
3. ~~**#59** - Animation system~~ - *Completed! (2025-07-05)*
4. **#6** - RenderTexture concept - *Extensive Overhaul*
5. ~~**#47** - New README.md for Alpha release~~ - *Completed*
- [x] **#78** - Middle Mouse Click sends "C" keyboard event - *Fixed*
- [x] **#77** - Fix error message copy/paste bug - *Fixed*
- [x] **#74** - Add missing `Grid.grid_y` property - *Fixed*
- [ ] **#37** - Fix Windows build module import from "scripts" directory - *Isolated Fix*
Issue #37 is **on hold** until we have a Windows build environment available. I actually suspect this is already fixed by the updates to the makefile, anyway.
- [x] **Entity Property Setters** - Fix "new style getargs format" error - *Fixed*
- [x] **Sprite Texture Setter** - Fix "error return without exception set" - *Fixed*
- [x] **keypressScene() Validation** - Add proper error handling - *Fixed*
### 🔄 Complete Iterator System
**Status**: Core iterators complete (#72 closed), Grid point iterators still pending
- [ ] **Grid Point Iterator Implementation** - Complete the remaining grid iteration work
- [x] **#73** - Add `entity.index()` method for collection removal - *Fixed*
- [x] **#69** ⚠️ **Alpha Blocker** - Refactor all collections to use Python Sequence Protocol - *Completed! (2025-07-05)*
**Dependencies**: Grid point iterators → #73 entity.index() → #69 Sequence Protocol overhaul
---
## 🗂 ISSUE TRIAGE BY SYSTEM (78 Total Issues)
### 🎮 Core Engine Systems
#### Iterator/Collection System (2 issues)
- [x] **#73** - Entity index() method for removal - *Fixed*
- [x] **#69** ⚠️ **Alpha Blocker** - Sequence Protocol refactor - *Completed! (2025-07-05)*
#### Python/C++ Integration (7 issues)
- [x] **#76** - UIEntity derived type preservation in collections - *Multiple Integrations*
- [ ] **#71** - Drawable base class hierarchy - *Extensive Overhaul*
- [ ] **#70** - PyPI wheel distribution - *Extensive Overhaul*
- [~] **#32** - Executable behave like `python` command - *Extensive Overhaul* *(90% Complete: -h, -V, -c, -m, -i, script execution, sys.argv, --exec all implemented. Only stdin (-) support missing)*
- [ ] **#35** - TCOD as built-in module - *Extensive Overhaul*
- [~] **#14** - Expose SFML as built-in module - *Research Complete, Implementation Pending*
- [ ] **#46** - Subinterpreter threading tests - *Multiple Integrations*
#### UI/Rendering System (12 issues)
- [x] **#63** ⚠️ **Alpha Blocker** - Z-order for UIDrawables - *Multiple Integrations*
- [x] **#59** ⚠️ **Alpha Blocker** - Animation system - *Completed! (2025-07-05)*
- [ ] **#6** ⚠️ **Alpha Blocker** - RenderTexture for all UIDrawables - *Extensive Overhaul*
- [ ] **#10** - UIDrawable visibility/AABB system - *Extensive Overhaul*
- [ ] **#8** - UIGrid RenderTexture viewport sizing - *Multiple Integrations*
- [x] **#9** - UIGrid RenderTexture resize handling - *Multiple Integrations*
- [ ] **#52** - UIGrid skip out-of-bounds entities - *Isolated Fix*
- [ ] **#50** - UIGrid background color field - *Isolated Fix*
- [ ] **#19** - Sprite get/set texture methods - *Multiple Integrations*
- [ ] **#17** - Move UISprite position into sf::Sprite - *Isolated Fix*
- [x] **#33** - Sprite index validation against texture range - *Fixed*
#### Grid/Entity System (6 issues)
- [ ] **#30** - Entity/Grid association management (.die() method) - *Extensive Overhaul*
- [ ] **#16** - Grid strict mode for entity knowledge/visibility - *Extensive Overhaul*
- [ ] **#67** - Grid stitching for infinite worlds - *Extensive Overhaul*
- [ ] **#15** - UIGridPointState cleanup and standardization - *Multiple Integrations*
- [ ] **#20** - UIGrid get_grid_size standardization - *Multiple Integrations*
- [x] **#12** - GridPoint/GridPointState forbid direct init - *Isolated Fix*
#### Scene/Window Management (5 issues)
- [x] **#61** - Scene object encapsulating key callbacks - *Completed Phase 5*
- [x] **#34** - Window object for resolution/scaling - *Completed Phase 5*
- [ ] **#62** - Multiple windows support - *Extensive Overhaul*
- [ ] **#49** - Window resolution & viewport controls - *Multiple Integrations*
- [x] **#1** - Scene resize event handling - *Completed Phase 5*
### 🔧 Quality of Life Features
#### UI Enhancement Features (8 issues)
- [ ] **#39** - Name field on UIDrawables - *Multiple Integrations*
- [ ] **#40** - `only_one` arg for unique naming - *Multiple Integrations*
- [ ] **#41** - `.find(name)` method for collections - *Multiple Integrations*
- [ ] **#38** - `children` arg for Frame initialization - *Isolated Fix*
- [ ] **#42** - Click callback arg for UIDrawable init - *Isolated Fix*
- [x] **#27** - UIEntityCollection.extend() method - *Fixed*
- [ ] **#28** - UICollectionIter for scene ui iteration - *Isolated Fix*
- [ ] **#26** - UIEntityCollectionIter implementation - *Isolated Fix*
### 🧹 Refactoring & Cleanup
#### Code Cleanup (7 issues)
- [x] **#3** ⚠️ **Alpha Blocker** - Remove `McRFPy_API::player_input` - *Completed*
- [x] **#2** ⚠️ **Alpha Blocker** - Review `registerPyAction` necessity - *Completed*
- [ ] **#7** - Remove unsafe no-argument constructors - *Multiple Integrations*
- [ ] **#21** - PyUIGrid dealloc cleanup - *Isolated Fix*
- [ ] **#75** - REPL thread separation from SFML window - *Multiple Integrations*
### 📚 Demo & Documentation
#### Documentation (2 issues)
- [x] **#47** ⚠️ **Alpha Blocker** - Alpha release README.md - *Isolated Fix*
- [ ] **#48** - Dependency compilation documentation - *Isolated Fix*
#### Demo Projects (6 issues)
- [ ] **#54** - Jupyter notebook integration demo - *Multiple Integrations*
- [ ] **#55** - Hunt the Wumpus AI demo - *Multiple Integrations*
- [ ] **#53** - Web interface input demo - *Multiple Integrations* *(New automation API could help)*
- [ ] **#45** - Accessibility mode demos - *Multiple Integrations* *(New automation API could help test)*
- [ ] **#36** - Dear ImGui integration tests - *Extensive Overhaul*
- [ ] **#65** - Python Explorer scene (replaces uitest) - *Extensive Overhaul*
---
## 🎮 STRATEGIC DIRECTION
### Engine Philosophy Maintained
- **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
---
## 📚 REFERENCES & CONTEXT
**Issue Dependencies** (Key Chains):
- Iterator System: Grid points → #73#69 (Alpha Blocker)
- UI Hierarchy: #71#63 (Alpha Blocker)
- Rendering: #6 (Alpha Blocker) → #8, #9#10
- Entity System: #30#16#67
- Window Management: #34#49, #61#62
**Commit References**:
- 167636c: Iterator improvements (UICollection/UIEntityCollection complete)
- Recent work: 7DRL 2025 completion, RPATH updates, console improvements
**Architecture Files**:
- Iterator patterns: src/UICollection.cpp, src/UIGrid.cpp
- Python integration: src/McRFPy_API.cpp, src/PyObjectUtils.h
- Game implementation: src/scripts/ (Crypt of Sokoban complete game)
---
## 🔮 FUTURE VISION: Pure Python Extension Architecture
### Concept: McRogueFace as a Traditional Python Package
**Status**: Unscheduled - 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
### Implementation Phases (If Pursued)
1. **Proof of Concept**: Simple SFML binding as Python extension
2. **Core Extraction**: Separate rendering from Python embedding
3. **Module Design**: Define clean API boundaries
4. **Incremental Migration**: Move systems one at a time
5. **Compatibility Layer**: Support existing games during transition
### 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.
---
## 🚀 IMMEDIATE NEXT STEPS (Priority Order)
### TODAY (July 12) - CRITICAL PATH:
1. **FIX ANIMATION BLOCKERS** for Tutorial Part 2:
- Implement input queueing during animations
- Add destination square reservation
- Test Pokemon-style continuous movement
2. **FIX GRID CLICKING** (discovered broken in headless):
- Uncomment and implement click events
- Add tile coordinate conversion
- Enable nested grid support
3. **CREATE TUTORIAL ANNOUNCEMENT** if blockers fixed
### Weekend (July 13-14) - Tutorial Sprint:
1. **Regenerate Parts 3-6** (machine drafts are broken)
2. **Create Parts 7-10**: Interface, Items, Targeting, Save/Load
3. **Create Parts 11-15**: Dungeon levels, difficulty, equipment
4. **Post more frequently during event** (narrator emphasis)
### Architecture Decision Log:
- **DECIDED**: Use three-layer architecture (visual/world/perspective)
- **DECIDED**: Spatial hashing over quadtrees for entities
- **DECIDED**: Batch operations are mandatory, not optional
- **DECIDED**: TCOD integration as mcrfpy.libtcod submodule
- **DECIDED**: Tutorial must showcase McRogueFace strengths, not mimic TCOD
### Risk Mitigation:
- **If TCOD integration delays**: Use pure Python FOV for tutorial
- **If performance issues**: Focus on <100x100 maps for demos
- **If tutorial incomplete**: Ship with 4 solid parts + roadmap
- **If bugs block progress**: Document as "known issues" and continue
---
## 📋 COMPREHENSIVE ISSUES FROM TRANSCRIPT ANALYSIS
### Animation System (6 issues)
1. **Input Queue During Animation**: Queue one additional move during animation
2. **Destination Square Reservation**: Block target when movement begins
3. **Pokemon-Style Movement**: Smooth continuous movement with input handling
4. **#119** - Animation Callbacks: Add completion callbacks with parameters
5. **#120** - Property Conflict Prevention: Prevent multiple animations on same property
6. **Remove Bare Pointers**: Complete refactoring to weak references ✅
### Grid System (6 issues)
7. **#111** - Grid Click Implementation: Fix commented-out events in headless
8. **Tile Coordinate Conversion**: Convert mouse to tile coordinates
9. **Nested Grid Support**: Enable clicking on grids within grids
10. **#123** - Grid Rendering Performance: Implement 256x256 subgrid system
11. **#116** - Dirty Flagging: Add dirty flag propagation from base
12. **#124** - Grid Point Animation: Enable animating individual tiles
### Python API (6 issues)
13. **Regenerate Python Bindings**: Create consistent interface generation
14. **#109** - Vector Class Enhancement: Add [0], [1] indexing to vectors
15. **#112** - Fix Object Splitting: Preserve Python derived class types
16. **#101/#110** - Standardize Constructors: Make all constructors consistent
17. **Color Class Bindings**: Properly expose SFML Color class
18. **Font Class Bindings**: Properly expose SFML Font class
### Architecture (8 issues)
19. **#118** - Scene as Drawable: Refactor Scene to inherit from Drawable
20. **Scene Visibility System**: Implement exclusive visibility switching
21. **Replace Transition System**: Use animations not special transitions
22. **#122** - Parent-Child UI: Add parent field to UI drawables
23. **Collection Methods**: Implement append/remove/extend with parent updates
24. **#121** - Timer Object System: Replace string-dictionary timers
25. **One-Shot Timer Mode**: Implement proper one-shot functionality
26. **Button Mechanics**: Any entity type can trigger buttons
### Entity System (4 issues)
27. **Step-On Entities**: Implement trigger when stepped on
28. **Bump Interaction**: Add bump-to-interact behavior
29. **Type-Aware Interactions**: Entity interactions based on type
30. **Button Mechanics**: Any entity can trigger buttons
### Tutorial & Documentation (4 issues)
31. **Fix Part 2 Tutorial**: Unblock with animation fixes
32. **Regenerate Parts 3-6**: Replace machine-generated content
33. **API Documentation**: Document ergonomic improvements
34. **Tutorial Alignment**: Ensure parts match TCOD structure
---
*Last Updated: 2025-07-12 (CRITICAL TUTORIAL SPRINT)*
*Next Review: July 15 after event start*

View File

@ -1,5 +1,7 @@
# McRogueFace API Reference # McRogueFace API Reference
*Generated on 2025-07-15 21:28:42*
## Overview ## Overview
McRogueFace Python API McRogueFace Python API
@ -373,14 +375,6 @@ A rectangular frame UI element that can contain other drawable elements.
#### Methods #### Methods
#### `get_bounds()`
Get the bounding rectangle of this drawable element.
**Returns:** tuple: (x, y, width, height) representing the element's bounds
**Note:** The bounds are in screen coordinates and account for current position and size.
#### `resize(width, height)` #### `resize(width, height)`
Resize the element to new dimensions. Resize the element to new dimensions.
@ -401,6 +395,14 @@ Move the element by a relative offset.
**Note:** This modifies the x and y position properties by the given amounts. **Note:** This modifies the x and y position properties by the given amounts.
#### `get_bounds()`
Get the bounding rectangle of this drawable element.
**Returns:** tuple: (x, y, width, height) representing the element's bounds
**Note:** The bounds are in screen coordinates and account for current position and size.
--- ---
### class `Caption` ### class `Caption`
@ -409,14 +411,6 @@ A text display UI element with customizable font and styling.
#### Methods #### Methods
#### `get_bounds()`
Get the bounding rectangle of this drawable element.
**Returns:** tuple: (x, y, width, height) representing the element's bounds
**Note:** The bounds are in screen coordinates and account for current position and size.
#### `resize(width, height)` #### `resize(width, height)`
Resize the element to new dimensions. Resize the element to new dimensions.
@ -437,6 +431,14 @@ Move the element by a relative offset.
**Note:** This modifies the x and y position properties by the given amounts. **Note:** This modifies the x and y position properties by the given amounts.
#### `get_bounds()`
Get the bounding rectangle of this drawable element.
**Returns:** tuple: (x, y, width, height) representing the element's bounds
**Note:** The bounds are in screen coordinates and account for current position and size.
--- ---
### class `Sprite` ### class `Sprite`
@ -445,14 +447,6 @@ A sprite UI element that displays a texture or portion of a texture atlas.
#### Methods #### Methods
#### `get_bounds()`
Get the bounding rectangle of this drawable element.
**Returns:** tuple: (x, y, width, height) representing the element's bounds
**Note:** The bounds are in screen coordinates and account for current position and size.
#### `resize(width, height)` #### `resize(width, height)`
Resize the element to new dimensions. Resize the element to new dimensions.
@ -473,6 +467,14 @@ Move the element by a relative offset.
**Note:** This modifies the x and y position properties by the given amounts. **Note:** This modifies the x and y position properties by the given amounts.
#### `get_bounds()`
Get the bounding rectangle of this drawable element.
**Returns:** tuple: (x, y, width, height) representing the element's bounds
**Note:** The bounds are in screen coordinates and account for current position and size.
--- ---
### class `Grid` ### class `Grid`
@ -481,6 +483,16 @@ A grid-based tilemap UI element for rendering tile-based levels and game worlds.
#### Methods #### Methods
#### `resize(width, height)`
Resize the element to new dimensions.
**Arguments:**
- `width` (*float*): New width in pixels
- `height` (*float*): New height in pixels
**Note:** For Caption and Sprite, this may not change actual size if determined by content.
#### `at(x, y)` #### `at(x, y)`
Get the GridPoint at the specified grid coordinates. Get the GridPoint at the specified grid coordinates.
@ -491,24 +503,6 @@ Get the GridPoint at the specified grid coordinates.
**Returns:** GridPoint or None: The grid point at (x, y), or None if out of bounds **Returns:** GridPoint or None: The grid point at (x, y), or None if out of bounds
#### `get_bounds()`
Get the bounding rectangle of this drawable element.
**Returns:** tuple: (x, y, width, height) representing the element's bounds
**Note:** The bounds are in screen coordinates and account for current position and size.
#### `resize(width, height)`
Resize the element to new dimensions.
**Arguments:**
- `width` (*float*): New width in pixels
- `height` (*float*): New height in pixels
**Note:** For Caption and Sprite, this may not change actual size if determined by content.
#### `move(dx, dy)` #### `move(dx, dy)`
Move the element by a relative offset. Move the element by a relative offset.
@ -519,6 +513,14 @@ Move the element by a relative offset.
**Note:** This modifies the x and y position properties by the given amounts. **Note:** This modifies the x and y position properties by the given amounts.
#### `get_bounds()`
Get the bounding rectangle of this drawable element.
**Returns:** tuple: (x, y, width, height) representing the element's bounds
**Note:** The bounds are in screen coordinates and account for current position and size.
--- ---
### class `Entity` ### class `Entity`
@ -527,12 +529,6 @@ Game entity that can be placed in a Grid.
#### Methods #### Methods
#### `die()`
Remove this entity from its parent grid.
**Note:** The entity object remains valid but is no longer rendered or updated.
#### `move(dx, dy)` #### `move(dx, dy)`
Move the element by a relative offset. Move the element by a relative offset.
@ -561,11 +557,11 @@ Get the bounding rectangle of this drawable element.
**Note:** The bounds are in screen coordinates and account for current position and size. **Note:** The bounds are in screen coordinates and account for current position and size.
#### `index()` #### `die()`
Get the index of this entity in its parent grid's entity list. Remove this entity from its parent grid.
**Returns:** int: Index position, or -1 if not in a grid **Note:** The entity object remains valid but is no longer rendered or updated.
#### `resize(width, height)` #### `resize(width, height)`
@ -577,6 +573,12 @@ Resize the element to new dimensions.
**Note:** For Caption and Sprite, this may not change actual size if determined by content. **Note:** For Caption and Sprite, this may not change actual size if determined by content.
#### `index()`
Get the index of this entity in its parent grid's entity list.
**Returns:** int: Index position, or -1 if not in a grid
--- ---
### Collections ### Collections
@ -587,13 +589,6 @@ Container for Entity objects in a Grid. Supports iteration and indexing.
#### Methods #### Methods
#### `append(entity)`
Add an entity to the end of the collection.
**Arguments:**
- `entity` (*Entity*): The entity to add
#### `remove(entity)` #### `remove(entity)`
Remove the first occurrence of an entity from the collection. Remove the first occurrence of an entity from the collection.
@ -603,6 +598,13 @@ Remove the first occurrence of an entity from the collection.
**Raises:** ValueError: If entity is not in collection **Raises:** ValueError: If entity is not in collection
#### `extend(iterable)`
Add all entities from an iterable to the collection.
**Arguments:**
- `iterable` (*Iterable[Entity]*): Entities to add
#### `count(entity)` #### `count(entity)`
Count the number of occurrences of an entity in the collection. Count the number of occurrences of an entity in the collection.
@ -623,12 +625,12 @@ Find the index of the first occurrence of an entity.
**Raises:** ValueError: If entity is not in collection **Raises:** ValueError: If entity is not in collection
#### `extend(iterable)` #### `append(entity)`
Add all entities from an iterable to the collection. Add an entity to the end of the collection.
**Arguments:** **Arguments:**
- `iterable` (*Iterable[Entity]*): Entities to add - `entity` (*Entity*): The entity to add
--- ---
@ -638,13 +640,6 @@ Container for UI drawable elements. Supports iteration and indexing.
#### Methods #### Methods
#### `append(drawable)`
Add a drawable element to the end of the collection.
**Arguments:**
- `drawable` (*UIDrawable*): The drawable element to add
#### `remove(drawable)` #### `remove(drawable)`
Remove the first occurrence of a drawable from the collection. Remove the first occurrence of a drawable from the collection.
@ -654,6 +649,13 @@ Remove the first occurrence of a drawable from the collection.
**Raises:** ValueError: If drawable is not in collection **Raises:** ValueError: If drawable is not in collection
#### `extend(iterable)`
Add all drawables from an iterable to the collection.
**Arguments:**
- `iterable` (*Iterable[UIDrawable]*): Drawables to add
#### `count(drawable)` #### `count(drawable)`
Count the number of occurrences of a drawable in the collection. Count the number of occurrences of a drawable in the collection.
@ -674,12 +676,12 @@ Find the index of the first occurrence of a drawable.
**Raises:** ValueError: If drawable is not in collection **Raises:** ValueError: If drawable is not in collection
#### `extend(iterable)` #### `append(drawable)`
Add all drawables from an iterable to the collection. Add a drawable element to the end of the collection.
**Arguments:** **Arguments:**
- `iterable` (*Iterable[UIDrawable]*): Drawables to add - `drawable` (*UIDrawable*): The drawable element to add
--- ---
@ -703,6 +705,17 @@ RGBA color representation.
#### Methods #### Methods
#### `to_hex()`
Convert this Color to a hexadecimal string.
**Returns:** str: Hex color string in format "#RRGGBB"
**Example:**
```python
hex_str = color.to_hex() # Returns "#FF0000"
```
#### `from_hex(hex_string)` #### `from_hex(hex_string)`
Create a Color from a hexadecimal color string. Create a Color from a hexadecimal color string.
@ -717,17 +730,6 @@ Create a Color from a hexadecimal color string.
red = Color.from_hex("#FF0000") red = Color.from_hex("#FF0000")
``` ```
#### `to_hex()`
Convert this Color to a hexadecimal string.
**Returns:** str: Hex color string in format "#RRGGBB"
**Example:**
```python
hex_str = color.to_hex() # Returns "#FF0000"
```
#### `lerp(other, t)` #### `lerp(other, t)`
Linearly interpolate between this color and another. Linearly interpolate between this color and another.
@ -757,14 +759,13 @@ Calculate the length/magnitude of this vector.
**Returns:** float: The magnitude of the vector **Returns:** float: The magnitude of the vector
#### `distance_to(other)` #### `normalize()`
Calculate the distance to another vector. Return a unit vector in the same direction.
**Arguments:** **Returns:** Vector: New normalized vector with magnitude 1.0
- `other` (*Vector*): The other vector
**Returns:** float: Distance between the two vectors **Raises:** ValueError: If vector has zero magnitude
#### `dot(other)` #### `dot(other)`
@ -775,6 +776,21 @@ Calculate the dot product with another vector.
**Returns:** float: Dot product of the two vectors **Returns:** float: Dot product of the two vectors
#### `distance_to(other)`
Calculate the distance to another vector.
**Arguments:**
- `other` (*Vector*): The other vector
**Returns:** float: Distance between the two vectors
#### `copy()`
Create a copy of this vector.
**Returns:** Vector: New Vector object with same x and y values
#### `angle()` #### `angle()`
Get the angle of this vector in radians. Get the angle of this vector in radians.
@ -789,20 +805,6 @@ Calculate the squared magnitude of this vector.
**Note:** Use this for comparisons to avoid expensive square root calculation. **Note:** Use this for comparisons to avoid expensive square root calculation.
#### `copy()`
Create a copy of this vector.
**Returns:** Vector: New Vector object with same x and y values
#### `normalize()`
Return a unit vector in the same direction.
**Returns:** Vector: New normalized vector with magnitude 1.0
**Raises:** ValueError: If vector has zero magnitude
--- ---
### class `Texture` ### class `Texture`
@ -834,6 +836,12 @@ Animate UI element properties over time.
#### Methods #### Methods
#### `get_current_value()`
Get the current interpolated value of the animation.
**Returns:** float: Current animation value between start and end
#### `update(delta_time)` #### `update(delta_time)`
Update the animation by the given time delta. Update the animation by the given time delta.
@ -852,12 +860,6 @@ Start the animation on a target UI element.
**Note:** The target must have the property specified in the animation constructor. **Note:** The target must have the property specified in the animation constructor.
#### `get_current_value()`
Get the current interpolated value of the animation.
**Returns:** float: Current animation value between start and end
--- ---
### class `Drawable` ### class `Drawable`
@ -866,14 +868,6 @@ Base class for all drawable UI elements.
#### Methods #### Methods
#### `get_bounds()`
Get the bounding rectangle of this drawable element.
**Returns:** tuple: (x, y, width, height) representing the element's bounds
**Note:** The bounds are in screen coordinates and account for current position and size.
#### `resize(width, height)` #### `resize(width, height)`
Resize the element to new dimensions. Resize the element to new dimensions.
@ -894,6 +888,14 @@ Move the element by a relative offset.
**Note:** This modifies the x and y position properties by the given amounts. **Note:** This modifies the x and y position properties by the given amounts.
#### `get_bounds()`
Get the bounding rectangle of this drawable element.
**Returns:** tuple: (x, y, width, height) representing the element's bounds
**Note:** The bounds are in screen coordinates and account for current position and size.
--- ---
### class `GridPoint` ### class `GridPoint`
@ -945,18 +947,18 @@ def handle_keyboard(key, action):
scene.register_keyboard(handle_keyboard) scene.register_keyboard(handle_keyboard)
``` ```
#### `activate()`
Make this scene the active scene.
**Note:** Equivalent to calling setScene() with this scene's name.
#### `get_ui()` #### `get_ui()`
Get the UI element collection for this scene. Get the UI element collection for this scene.
**Returns:** UICollection: Collection of all UI elements in this scene **Returns:** UICollection: Collection of all UI elements in this scene
#### `activate()`
Make this scene the active scene.
**Note:** Equivalent to calling setScene() with this scene's name.
#### `keypress(handler)` #### `keypress(handler)`
Register a keyboard handler function for this scene. Register a keyboard handler function for this scene.
@ -974,18 +976,6 @@ Timer object for scheduled callbacks.
#### Methods #### Methods
#### `restart()`
Restart the timer from the beginning.
**Note:** Resets the timer's internal clock to zero.
#### `cancel()`
Cancel the timer and remove it from the system.
**Note:** After cancelling, the timer object cannot be reused.
#### `pause()` #### `pause()`
Pause the timer, stopping its callback execution. Pause the timer, stopping its callback execution.
@ -998,6 +988,18 @@ Resume a paused timer.
**Note:** Has no effect if timer is not paused. **Note:** Has no effect if timer is not paused.
#### `restart()`
Restart the timer from the beginning.
**Note:** Resets the timer's internal clock to zero.
#### `cancel()`
Cancel the timer and remove it from the system.
**Note:** After cancelling, the timer object cannot be reused.
--- ---
### class `Window` ### class `Window`
@ -1006,14 +1008,6 @@ Window singleton for accessing and modifying the game window properties.
#### Methods #### Methods
#### `get()`
Get the Window singleton instance.
**Returns:** Window: The singleton window object
**Note:** This is a static method that returns the same instance every time.
#### `screenshot(filename)` #### `screenshot(filename)`
Take a screenshot and save it to a file. Take a screenshot and save it to a file.
@ -1023,6 +1017,14 @@ Take a screenshot and save it to a file.
**Note:** Supports PNG, JPG, and BMP formats based on file extension. **Note:** Supports PNG, JPG, and BMP formats based on file extension.
#### `get()`
Get the Window singleton instance.
**Returns:** Window: The singleton window object
**Note:** This is a static method that returns the same instance every time.
#### `center()` #### `center()`
Center the window on the screen. Center the window on the screen.

View File

@ -108,7 +108,7 @@
<body> <body>
<div class="container"> <div class="container">
<h1>McRogueFace API Reference</h1> <h1>McRogueFace API Reference</h1>
<p><em>Generated on 2025-07-10 01:13:53</em></p> <p><em>Generated on 2025-07-15 21:28:24</em></p>
<p><em>This documentation was dynamically generated from the compiled module.</em></p> <p><em>This documentation was dynamically generated from the compiled module.</em></p>
<div class="toc"> <div class="toc">
@ -350,14 +350,31 @@ Note:</p>
<p>Animation object for animating UI properties</p> <p>Animation object for animating UI properties</p>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">completecomplete() -> None</code></h5>
<p>Complete the animation immediately by jumping to the final value.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">get_current_value(...)</code></h5> <h5><code class="method-name">get_current_value(...)</code></h5>
<p>Get the current interpolated value</p> <p>Get the current interpolated value</p>
</div> </div>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">start(...)</code></h5> <h5><code class="method-name">hasValidTargethasValidTarget() -> bool</code></h5>
<p>Start the animation on a target UIDrawable</p> <p>Check if the animation still has a valid target.</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> True if the target still exists, False if it was destroyed.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">startstart(target) -> None</code></h5>
<p>Start the animation on a target UI element.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>target</span>: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)</div>
</div>
</div> </div>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -368,29 +385,41 @@ Note:</p>
<div class="method-section"> <div class="method-section">
<h3 id="Caption"><span class="class-name">Caption</span></h3> <h3 id="Caption"><span class="class-name">Caption</span></h3>
<p><em>Inherits from: Drawable</em></p> <p><em>Inherits from: Drawable</em></p>
<p>Caption(text=&#x27;&#x27;, x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None) <p>Caption(pos=None, font=None, text=&#x27;&#x27;, **kwargs)
A text display UI element with customizable font and styling. A text display UI element with customizable font and styling.
Args: Args:
text (str): The text content to display. Default: &#x27;&#x27; pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)
x (float): X position in pixels. Default: 0 font (Font, optional): Font object for text rendering. Default: engine default font
y (float): Y position in pixels. Default: 0 text (str, optional): The text content to display. Default: &#x27;&#x27;
font (Font): Font object for text rendering. Default: engine default font
Keyword Args:
fill_color (Color): Text fill color. Default: (255, 255, 255, 255) fill_color (Color): Text fill color. Default: (255, 255, 255, 255)
outline_color (Color): Text outline color. Default: (0, 0, 0, 255) outline_color (Color): Text outline color. Default: (0, 0, 0, 255)
outline (float): Text outline thickness. Default: 0 outline (float): Text outline thickness. Default: 0
font_size (float): Font size in points. Default: 16
click (callable): Click event handler. Default: None click (callable): Click event handler. Default: None
visible (bool): Visibility state. Default: True
opacity (float): Opacity (0.0-1.0). Default: 1.0
z_index (int): Rendering order. Default: 0
name (str): Element name for finding. Default: None
x (float): X position override. Default: 0
y (float): Y position override. Default: 0
Attributes: Attributes:
text (str): The displayed text content text (str): The displayed text content
x, y (float): Position in pixels x, y (float): Position in pixels
pos (Vector): Position as a Vector object
font (Font): Font used for rendering font (Font): Font used for rendering
font_size (float): Font size in points
fill_color, outline_color (Color): Text appearance fill_color, outline_color (Color): Text appearance
outline (float): Outline thickness outline (float): Outline thickness
click (callable): Click event handler click (callable): Click event handler
visible (bool): Visibility state visible (bool): Visibility state
opacity (float): Opacity value
z_index (int): Rendering order z_index (int): Rendering order
name (str): Element name
w, h (float): Read-only computed size based on text and font</p> w, h (float): Read-only computed size based on text and font</p>
<h4>Methods:</h4> <h4>Methods:</h4>
@ -447,8 +476,32 @@ Attributes:
<div class="method-section"> <div class="method-section">
<h3 id="Entity"><span class="class-name">Entity</span></h3> <h3 id="Entity"><span class="class-name">Entity</span></h3>
<p><em>Inherits from: Drawable</em></p> <p>Entity(grid_pos=None, texture=None, sprite_index=0, **kwargs)
<p>UIEntity objects</p>
A game entity that exists on a grid with sprite rendering.
Args:
grid_pos (tuple, optional): Grid position as (x, y) tuple. Default: (0, 0)
texture (Texture, optional): Texture object for sprite. Default: default texture
sprite_index (int, optional): Index into texture atlas. Default: 0
Keyword Args:
grid (Grid): Grid to attach entity to. Default: None
visible (bool): Visibility state. Default: True
opacity (float): Opacity (0.0-1.0). Default: 1.0
name (str): Element name for finding. Default: None
x (float): X grid position override. Default: 0
y (float): Y grid position override. Default: 0
Attributes:
pos (tuple): Grid position as (x, y) tuple
x, y (float): Grid position coordinates
draw_pos (tuple): Pixel position for rendering
gridstate (GridPointState): Visibility state for grid points
sprite_index (int): Current sprite index
visible (bool): Visibility state
opacity (float): Opacity value
name (str): Element name</p>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -532,30 +585,42 @@ when the entity moves if it has a grid with perspective set.</p>
<div class="method-section"> <div class="method-section">
<h3 id="Frame"><span class="class-name">Frame</span></h3> <h3 id="Frame"><span class="class-name">Frame</span></h3>
<p><em>Inherits from: Drawable</em></p> <p><em>Inherits from: Drawable</em></p>
<p>Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None) <p>Frame(pos=None, size=None, **kwargs)
A rectangular frame UI element that can contain other drawable elements. A rectangular frame UI element that can contain other drawable elements.
Args: Args:
x (float): X position in pixels. Default: 0 pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)
y (float): Y position in pixels. Default: 0 size (tuple, optional): Size as (width, height) tuple. Default: (0, 0)
w (float): Width in pixels. Default: 0
h (float): Height in pixels. Default: 0 Keyword Args:
fill_color (Color): Background fill color. Default: (0, 0, 0, 128) fill_color (Color): Background fill color. Default: (0, 0, 0, 128)
outline_color (Color): Border outline color. Default: (255, 255, 255, 255) outline_color (Color): Border outline color. Default: (255, 255, 255, 255)
outline (float): Border outline thickness. Default: 0 outline (float): Border outline thickness. Default: 0
click (callable): Click event handler. Default: None click (callable): Click event handler. Default: None
children (list): Initial list of child drawable elements. Default: None children (list): Initial list of child drawable elements. Default: None
visible (bool): Visibility state. Default: True
opacity (float): Opacity (0.0-1.0). Default: 1.0
z_index (int): Rendering order. Default: 0
name (str): Element name for finding. Default: None
x (float): X position override. Default: 0
y (float): Y position override. Default: 0
w (float): Width override. Default: 0
h (float): Height override. Default: 0
clip_children (bool): Whether to clip children to frame bounds. Default: False
Attributes: Attributes:
x, y (float): Position in pixels x, y (float): Position in pixels
w, h (float): Size in pixels w, h (float): Size in pixels
pos (Vector): Position as a Vector object
fill_color, outline_color (Color): Visual appearance fill_color, outline_color (Color): Visual appearance
outline (float): Border thickness outline (float): Border thickness
click (callable): Click event handler click (callable): Click event handler
children (list): Collection of child drawable elements children (list): Collection of child drawable elements
visible (bool): Visibility state visible (bool): Visibility state
opacity (float): Opacity value
z_index (int): Rendering order z_index (int): Rendering order
name (str): Element name
clip_children (bool): Whether to clip children to frame bounds</p> clip_children (bool): Whether to clip children to frame bounds</p>
<h4>Methods:</h4> <h4>Methods:</h4>
@ -575,32 +640,53 @@ Attributes:
<div class="method-section"> <div class="method-section">
<h3 id="Grid"><span class="class-name">Grid</span></h3> <h3 id="Grid"><span class="class-name">Grid</span></h3>
<p><em>Inherits from: Drawable</em></p> <p><em>Inherits from: Drawable</em></p>
<p>Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None) <p>Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)
A grid-based tilemap UI element for rendering tile-based levels and game worlds. A grid-based UI element for tile-based rendering and entity management.
Args: Args:
x (float): X position in pixels. Default: 0 pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)
y (float): Y position in pixels. Default: 0 size (tuple, optional): Size as (width, height) tuple. Default: auto-calculated from grid_size
grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20) grid_size (tuple, optional): Grid dimensions as (grid_x, grid_y) tuple. Default: (2, 2)
texture (Texture): Texture atlas containing tile sprites. Default: None texture (Texture, optional): Texture containing tile sprites. Default: default texture
tile_width (int): Width of each tile in pixels. Default: 16
tile_height (int): Height of each tile in pixels. Default: 16 Keyword Args:
scale (float): Grid scaling factor. Default: 1.0 fill_color (Color): Background fill color. Default: None
click (callable): Click event handler. Default: None click (callable): Click event handler. Default: None
center_x (float): X coordinate of center point. Default: 0
center_y (float): Y coordinate of center point. Default: 0
zoom (float): Zoom level for rendering. Default: 1.0
perspective (int): Entity perspective index (-1 for omniscient). Default: -1
visible (bool): Visibility state. Default: True
opacity (float): Opacity (0.0-1.0). Default: 1.0
z_index (int): Rendering order. Default: 0
name (str): Element name for finding. Default: None
x (float): X position override. Default: 0
y (float): Y position override. Default: 0
w (float): Width override. Default: auto-calculated
h (float): Height override. Default: auto-calculated
grid_x (int): Grid width override. Default: 2
grid_y (int): Grid height override. Default: 2
Attributes: Attributes:
x, y (float): Position in pixels x, y (float): Position in pixels
w, h (float): Size in pixels
pos (Vector): Position as a Vector object
size (tuple): Size as (width, height) tuple
center (tuple): Center point as (x, y) tuple
center_x, center_y (float): Center point coordinates
zoom (float): Zoom level for rendering
grid_size (tuple): Grid dimensions (width, height) in tiles grid_size (tuple): Grid dimensions (width, height) in tiles
tile_width, tile_height (int): Tile dimensions in pixels grid_x, grid_y (int): Grid dimensions
texture (Texture): Tile texture atlas texture (Texture): Tile texture atlas
scale (float): Scale multiplier fill_color (Color): Background color
points (list): 2D array of GridPoint objects for tile data entities (EntityCollection): Collection of entities in the grid
entities (list): Collection of Entity objects in the grid perspective (int): Entity perspective index
background_color (Color): Grid background color
click (callable): Click event handler click (callable): Click event handler
visible (bool): Visibility state visible (bool): Visibility state
z_index (int): Rendering order</p> opacity (float): Opacity value
z_index (int): Rendering order
name (str): Element name</p>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
@ -733,26 +819,39 @@ Attributes:
<div class="method-section"> <div class="method-section">
<h3 id="Sprite"><span class="class-name">Sprite</span></h3> <h3 id="Sprite"><span class="class-name">Sprite</span></h3>
<p><em>Inherits from: Drawable</em></p> <p><em>Inherits from: Drawable</em></p>
<p>Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None) <p>Sprite(pos=None, texture=None, sprite_index=0, **kwargs)
A sprite UI element that displays a texture or portion of a texture atlas. A sprite UI element that displays a texture or portion of a texture atlas.
Args: Args:
x (float): X position in pixels. Default: 0 pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)
y (float): Y position in pixels. Default: 0 texture (Texture, optional): Texture object to display. Default: default texture
texture (Texture): Texture object to display. Default: None sprite_index (int, optional): Index into texture atlas. Default: 0
sprite_index (int): Index into texture atlas (if applicable). Default: 0
scale (float): Sprite scaling factor. Default: 1.0 Keyword Args:
scale (float): Uniform scale factor. Default: 1.0
scale_x (float): Horizontal scale factor. Default: 1.0
scale_y (float): Vertical scale factor. Default: 1.0
click (callable): Click event handler. Default: None click (callable): Click event handler. Default: None
visible (bool): Visibility state. Default: True
opacity (float): Opacity (0.0-1.0). Default: 1.0
z_index (int): Rendering order. Default: 0
name (str): Element name for finding. Default: None
x (float): X position override. Default: 0
y (float): Y position override. Default: 0
Attributes: Attributes:
x, y (float): Position in pixels x, y (float): Position in pixels
pos (Vector): Position as a Vector object
texture (Texture): The texture being displayed texture (Texture): The texture being displayed
sprite_index (int): Current sprite index in texture atlas sprite_index (int): Current sprite index in texture atlas
scale (float): Scale multiplier scale (float): Uniform scale factor
scale_x, scale_y (float): Individual scale factors
click (callable): Click event handler click (callable): Click event handler
visible (bool): Visibility state visible (bool): Visibility state
opacity (float): Opacity value
z_index (int): Rendering order z_index (int): Rendering order
name (str): Element name
w, h (float): Read-only computed size based on texture and scale</p> w, h (float): Read-only computed size based on texture and scale</p>
<h4>Methods:</h4> <h4>Methods:</h4>
@ -777,27 +876,64 @@ Attributes:
<div class="method-section"> <div class="method-section">
<h3 id="Timer"><span class="class-name">Timer</span></h3> <h3 id="Timer"><span class="class-name">Timer</span></h3>
<p>Timer object for scheduled callbacks</p> <p>Timer(name, callback, interval, once=False)
Create a timer that calls a function at regular intervals.
Args:
name (str): Unique identifier for the timer
callback (callable): Function to call - receives (timer, runtime) args
interval (int): Time between calls in milliseconds
once (bool): If True, timer stops after first call. Default: False
Attributes:
interval (int): Time between calls in milliseconds
remaining (int): Time until next call in milliseconds (read-only)
paused (bool): Whether timer is paused (read-only)
active (bool): Whether timer is active and not paused (read-only)
callback (callable): The callback function
once (bool): Whether timer stops after firing once
Methods:
pause(): Pause the timer, preserving time remaining
resume(): Resume a paused timer
cancel(): Stop and remove the timer
restart(): Reset timer to start from beginning
Example:
def on_timer(timer, runtime):
print(f&#x27;Timer {timer} fired at {runtime}ms&#x27;)
if runtime &gt; 5000:
timer.cancel()
timer = mcrfpy.Timer(&#x27;my_timer&#x27;, on_timer, 1000)
timer.pause() # Pause timer
timer.resume() # Resume timer
timer.once = True # Make it one-shot</p>
<h4>Methods:</h4> <h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">cancel(...)</code></h5> <h5><code class="method-name">cancelcancel() -> None</code></h5>
<p>Cancel the timer and remove it from the system</p> <p>Cancel the timer and remove it from the timer system.
The timer will no longer fire and cannot be restarted.</p>
</div> </div>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">pause(...)</code></h5> <h5><code class="method-name">pausepause() -> None</code></h5>
<p>Pause the timer</p> <p>Pause the timer, preserving the time remaining until next trigger.
The timer can be resumed later with resume().</p>
</div> </div>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">restart(...)</code></h5> <h5><code class="method-name">restartrestart() -> None</code></h5>
<p>Restart the timer from the current time</p> <p>Restart the timer from the beginning.
Resets the timer to fire after a full interval from now.</p>
</div> </div>
<div style="margin-left: 20px; margin-bottom: 15px;"> <div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">resume(...)</code></h5> <h5><code class="method-name">resumeresume() -> None</code></h5>
<p>Resume a paused timer</p> <p>Resume a paused timer from where it left off.
Has no effect if the timer is not paused.</p>
</div> </div>
</div> </div>

View File

@ -1,209 +1,532 @@
"""Type stubs for McRogueFace Python API. """Type stubs for McRogueFace Python API.
Auto-generated - do not edit directly. Core game engine interface for creating roguelike games with Python.
""" """
from typing import Any, List, Dict, Tuple, Optional, Callable, Union from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
# Module documentation # Type aliases
# 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 UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid']
Transition = Union[str, None]
# Classes # 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: class Color:
"""SFML Color Object""" """SFML Color Object for RGBA colors."""
def __init__(selftype(self)) -> None: ...
r: int
def from_hex(selfe.g., '#FF0000' or 'FF0000') -> Any: ... g: int
def lerp(self, *args, **kwargs) -> Any: ... b: int
def to_hex(self, *args, **kwargs) -> Any: ... a: int
class Drawable: @overload
"""Base class for all drawable UI elements""" def __init__(self) -> None: ...
def __init__(selftype(self)) -> None: ... @overload
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ... def from_hex(self, hex_string: str) -> 'Color':
def resize(selfwidth, height) -> Any: ... """Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
...
class Entity:
"""UIEntity objects""" def to_hex(self) -> str:
def __init__(selftype(self)) -> None: ... """Convert color to hex string format."""
...
def at(self, *args, **kwargs) -> Any: ...
def die(self, *args, **kwargs) -> Any: ... def lerp(self, other: 'Color', t: float) -> 'Color':
def get_bounds(selfx, y, width, height) -> Any: ... """Linear interpolation between two colors."""
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: class Vector:
"""SFML Vector Object""" """SFML Vector Object for 2D coordinates."""
def __init__(selftype(self)) -> None: ...
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: ...
def angle(self, *args, **kwargs) -> Any: ... class Texture:
def copy(self, *args, **kwargs) -> Any: ... """SFML Texture Object for images."""
def distance_to(self, *args, **kwargs) -> Any: ...
def dot(self, *args, **kwargs) -> Any: ... def __init__(self, filename: str) -> None: ...
def magnitude(self, *args, **kwargs) -> Any: ...
def magnitude_squared(self, *args, **kwargs) -> Any: ... filename: str
def normalize(self, *args, **kwargs) -> Any: ... 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: class Window:
"""Window singleton for accessing and modifying the game window properties""" """Window singleton for managing the game window."""
def __init__(selftype(self)) -> None: ...
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."""
...
def center(self, *args, **kwargs) -> Any: ... class Animation:
def get(self, *args, **kwargs) -> Any: ... """Animation object for animating UI properties."""
def screenshot(self, *args, **kwargs) -> Any: ...
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."""
...
# Functions # Module functions
def createScene(name: str) -> None: ... def createSoundBuffer(filename: str) -> int:
def createSoundBuffer(filename: str) -> int: ... """Load a sound effect from a file and return its buffer ID."""
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 def loadMusic(filename: str) -> None:
"""Load and immediately play background music from a file."""
...
FOV_BASIC: int def setMusicVolume(volume: int) -> None:
FOV_DIAMOND: int """Set the global music volume (0-100)."""
FOV_PERMISSIVE_0: int ...
FOV_PERMISSIVE_1: int
FOV_PERMISSIVE_2: int def setSoundVolume(volume: int) -> None:
FOV_PERMISSIVE_3: int """Set the global sound effects volume (0-100)."""
FOV_PERMISSIVE_4: int ...
FOV_PERMISSIVE_5: int
FOV_PERMISSIVE_6: int def playSound(buffer_id: int) -> None:
FOV_PERMISSIVE_7: int """Play a sound effect using a previously loaded buffer."""
FOV_PERMISSIVE_8: int ...
FOV_RESTRICTIVE: int
FOV_SHADOW: int def getMusicVolume() -> int:
default_font: Any """Get the current music volume level (0-100)."""
default_texture: Any ...
def getSoundVolume() -> int:
"""Get the current sound effects volume level (0-100)."""
...
def sceneUI(scene: Optional[str] = None) -> UICollection:
"""Get all UI elements for a scene."""
...
def currentScene() -> str:
"""Get the name of the currently active scene."""
...
def setScene(scene: str, transition: Optional[str] = None, duration: float = 0.0) -> None:
"""Switch to a different scene with optional transition effect."""
...
def createScene(name: str) -> None:
"""Create a new empty scene."""
...
def keypressScene(handler: Callable[[str, bool], None]) -> None:
"""Set the keyboard event handler for the current scene."""
...
def setTimer(name: str, handler: Callable[[float], None], interval: int) -> None:
"""Create or update a recurring timer."""
...
def delTimer(name: str) -> None:
"""Stop and remove a timer."""
...
def exit() -> None:
"""Cleanly shut down the game engine and exit the application."""
...
def setScale(multiplier: float) -> None:
"""Scale the game window size (deprecated - use Window.resolution)."""
...
def find(name: str, scene: Optional[str] = None) -> Optional[UIElement]:
"""Find the first UI element with the specified name."""
...
def findAll(pattern: str, scene: Optional[str] = None) -> List[UIElement]:
"""Find all UI elements matching a name pattern (supports * wildcards)."""
...
def getMetrics() -> Dict[str, Union[int, float]]:
"""Get current performance metrics."""
...
# Submodule
class automation:
"""Automation API for testing and scripting."""
@staticmethod
def screenshot(filename: str) -> bool:
"""Save a screenshot to the specified file."""
...
@staticmethod
def position() -> Tuple[int, int]:
"""Get current mouse position as (x, y) tuple."""
...
@staticmethod
def size() -> Tuple[int, int]:
"""Get screen size as (width, height) tuple."""
...
@staticmethod
def onScreen(x: int, y: int) -> bool:
"""Check if coordinates are within screen bounds."""
...
@staticmethod
def moveTo(x: int, y: int, duration: float = 0.0) -> None:
"""Move mouse to absolute position."""
...
@staticmethod
def moveRel(xOffset: int, yOffset: int, duration: float = 0.0) -> None:
"""Move mouse relative to current position."""
...
@staticmethod
def dragTo(x: int, y: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse to position."""
...
@staticmethod
def dragRel(xOffset: int, yOffset: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse relative to current position."""
...
@staticmethod
def click(x: Optional[int] = None, y: Optional[int] = None, clicks: int = 1,
interval: float = 0.0, button: str = 'left') -> None:
"""Click mouse at position."""
...
@staticmethod
def mouseDown(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Press mouse button down."""
...
@staticmethod
def mouseUp(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Release mouse button."""
...
@staticmethod
def keyDown(key: str) -> None:
"""Press key down."""
...
@staticmethod
def keyUp(key: str) -> None:
"""Release key."""
...
@staticmethod
def press(key: str) -> None:
"""Press and release a key."""
...
@staticmethod
def typewrite(text: str, interval: float = 0.0) -> None:
"""Type text with optional interval between characters."""
...

View File

@ -3,6 +3,7 @@
#include "UIEntity.h" #include "UIEntity.h"
#include "PyAnimation.h" #include "PyAnimation.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PythonObjectCache.h"
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <unordered_map> #include <unordered_map>
@ -46,6 +47,11 @@ Animation::~Animation() {
Py_DECREF(callback); Py_DECREF(callback);
PyGILState_Release(gstate); PyGILState_Release(gstate);
} }
// Clean up cache entry
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
} }
void Animation::start(std::shared_ptr<UIDrawable> target) { void Animation::start(std::shared_ptr<UIDrawable> target) {

View File

@ -90,6 +90,9 @@ private:
bool callbackTriggered = false; // Ensure callback only fires once bool callbackTriggered = false; // Ensure callback only fires once
PyObject* pyAnimationWrapper = nullptr; // Weak reference to PyAnimation if created from Python 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 // Helper to interpolate between values
AnimationValue interpolate(float t) const; AnimationValue interpolate(float t) const;

View File

@ -5,6 +5,7 @@
#include "UITestScene.h" #include "UITestScene.h"
#include "Resources.h" #include "Resources.h"
#include "Animation.h" #include "Animation.h"
#include "Timer.h"
#include <cmath> #include <cmath>
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{}) GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
@ -275,7 +276,7 @@ void GameEngine::run()
cleanup(); cleanup();
} }
std::shared_ptr<PyTimerCallable> GameEngine::getTimer(const std::string& name) std::shared_ptr<Timer> GameEngine::getTimer(const std::string& name)
{ {
auto it = timers.find(name); auto it = timers.find(name);
if (it != timers.end()) { if (it != timers.end()) {
@ -293,7 +294,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
{ {
// Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check // Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check
// see gitea issue #4: this allows for a timer to be deleted during its own call to itself // see gitea issue #4: this allows for a timer to be deleted during its own call to itself
timers[name] = std::make_shared<PyTimerCallable>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds()); timers[name] = std::make_shared<Timer>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds());
return; return;
} }
} }
@ -302,7 +303,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl; std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl;
return; return;
} }
timers[name] = std::make_shared<PyTimerCallable>(target, interval, runtime.getElapsedTime().asMilliseconds()); timers[name] = std::make_shared<Timer>(target, interval, runtime.getElapsedTime().asMilliseconds());
} }
void GameEngine::testTimers() void GameEngine::testTimers()
@ -313,7 +314,8 @@ void GameEngine::testTimers()
{ {
it->second->test(now); it->second->test(now);
if (it->second->isNone()) // Remove timers that have been cancelled or are one-shot and fired
if (!it->second->getCallback() || it->second->getCallback() == Py_None)
{ {
it = timers.erase(it); it = timers.erase(it);
} }

View File

@ -58,8 +58,7 @@ private:
public: public:
sf::Clock runtime; sf::Clock runtime;
//std::map<std::string, Timer> timers; std::map<std::string, std::shared_ptr<Timer>> timers;
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
std::string scene; std::string scene;
// Profiling metrics // Profiling metrics
@ -116,7 +115,7 @@ public:
float getFrameTime() { return frameTime; } float getFrameTime() { return frameTime; }
sf::View getView() { return visible; } sf::View getView() { return visible; }
void manageTimer(std::string, PyObject*, int); void manageTimer(std::string, PyObject*, int);
std::shared_ptr<PyTimerCallable> getTimer(const std::string& name); std::shared_ptr<Timer> getTimer(const std::string& name);
void setWindowScale(float); void setWindowScale(float);
bool isHeadless() const { return headless; } bool isHeadless() const { return headless; }
void processEvent(const sf::Event& event); void processEvent(const sf::Event& event);

View File

@ -267,6 +267,14 @@ PyObject* PyInit_mcrfpy()
PySceneType.tp_methods = PySceneClass::methods; PySceneType.tp_methods = PySceneClass::methods;
PySceneType.tp_getset = PySceneClass::getsetters; PySceneType.tp_getset = PySceneClass::getsetters;
// Set up weakref support for all types that need it
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
PyUICaptionType.tp_weaklistoffset = offsetof(PyUICaptionObject, weakreflist);
PyUISpriteType.tp_weaklistoffset = offsetof(PyUISpriteObject, weakreflist);
PyUIGridType.tp_weaklistoffset = offsetof(PyUIGridObject, weakreflist);
PyUIEntityType.tp_weaklistoffset = offsetof(PyUIEntityObject, weakreflist);
int i = 0; int i = 0;
auto t = pytypes[i]; auto t = pytypes[i];
while (t != nullptr) while (t != nullptr)

View File

@ -5,6 +5,21 @@ PyCallable::PyCallable(PyObject* _target)
target = Py_XNewRef(_target); target = Py_XNewRef(_target);
} }
PyCallable::PyCallable(const PyCallable& other)
{
target = Py_XNewRef(other.target);
}
PyCallable& PyCallable::operator=(const PyCallable& other)
{
if (this != &other) {
PyObject* old_target = target;
target = Py_XNewRef(other.target);
Py_XDECREF(old_target);
}
return *this;
}
PyCallable::~PyCallable() PyCallable::~PyCallable()
{ {
if (target) if (target)
@ -21,103 +36,6 @@ bool PyCallable::isNone() const
return (target == Py_None || target == NULL); return (target == Py_None || target == NULL);
} }
PyTimerCallable::PyTimerCallable(PyObject* _target, int _interval, int now)
: PyCallable(_target), interval(_interval), last_ran(now),
paused(false), pause_start_time(0), total_paused_time(0)
{}
PyTimerCallable::PyTimerCallable()
: PyCallable(Py_None), interval(0), last_ran(0),
paused(false), pause_start_time(0), total_paused_time(0)
{}
bool PyTimerCallable::hasElapsed(int now)
{
if (paused) return false;
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;
}
void PyTimerCallable::pause(int current_time)
{
if (!paused) {
paused = true;
pause_start_time = current_time;
}
}
void PyTimerCallable::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 PyTimerCallable::restart(int current_time)
{
last_ran = current_time;
paused = false;
pause_start_time = 0;
total_paused_time = 0;
}
void PyTimerCallable::cancel()
{
// Cancel by setting target to None
if (target && target != Py_None) {
Py_DECREF(target);
}
target = Py_None;
Py_INCREF(Py_None);
}
int PyTimerCallable::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;
}
void PyTimerCallable::setCallback(PyObject* new_callback)
{
if (target && target != Py_None) {
Py_DECREF(target);
}
target = Py_XNewRef(new_callback);
}
PyClickCallable::PyClickCallable(PyObject* _target) PyClickCallable::PyClickCallable(PyObject* _target)
: PyCallable(_target) : PyCallable(_target)

View File

@ -6,45 +6,15 @@ class PyCallable
{ {
protected: protected:
PyObject* target; PyObject* target;
public:
PyCallable(PyObject*); PyCallable(PyObject*);
PyCallable(const PyCallable& other);
PyCallable& operator=(const PyCallable& other);
~PyCallable(); ~PyCallable();
PyObject* call(PyObject*, PyObject*); PyObject* call(PyObject*, PyObject*);
public:
bool isNone() const; bool isNone() const;
}; PyObject* borrow() const { return target; }
class PyTimerCallable: public PyCallable
{
private:
int interval;
int last_ran;
void call(int);
// Pause/resume support
bool paused;
int pause_start_time;
int total_paused_time;
public:
bool hasElapsed(int);
bool test(int);
PyTimerCallable(PyObject*, int, int);
PyTimerCallable();
// Timer control methods
void pause(int current_time);
void resume(int current_time);
void restart(int current_time);
void cancel();
// Timer state queries
bool isPaused() const { return paused; }
bool isActive() const { return !isNone() && !paused; }
int getInterval() const { return interval; }
void setInterval(int new_interval) { interval = new_interval; }
int getRemaining(int current_time) const;
PyObject* getCallback() { return target; }
void setCallback(PyObject* new_callback);
}; };
class PyClickCallable: public PyCallable class PyClickCallable: public PyCallable
@ -54,6 +24,11 @@ public:
PyObject* borrow(); PyObject* borrow();
PyClickCallable(PyObject*); PyClickCallable(PyObject*);
PyClickCallable(); PyClickCallable();
PyClickCallable(const PyClickCallable& other) : PyCallable(other) {}
PyClickCallable& operator=(const PyClickCallable& other) {
PyCallable::operator=(other);
return *this;
}
}; };
class PyKeyCallable: public PyCallable class PyKeyCallable: public PyCallable

View File

@ -1,7 +1,8 @@
#include "PyTimer.h" #include "PyTimer.h"
#include "PyCallable.h" #include "Timer.h"
#include "GameEngine.h" #include "GameEngine.h"
#include "Resources.h" #include "Resources.h"
#include "PythonObjectCache.h"
#include <sstream> #include <sstream>
PyObject* PyTimer::repr(PyObject* self) { PyObject* PyTimer::repr(PyObject* self) {
@ -11,7 +12,22 @@ PyObject* PyTimer::repr(PyObject* self) {
if (timer->data) { if (timer->data) {
oss << "interval=" << timer->data->getInterval() << "ms "; oss << "interval=" << timer->data->getInterval() << "ms ";
oss << (timer->data->isPaused() ? "paused" : "active"); 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 { } else {
oss << "uninitialized"; oss << "uninitialized";
} }
@ -25,18 +41,20 @@ PyObject* PyTimer::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
if (self) { if (self) {
new(&self->name) std::string(); // Placement new for std::string new(&self->name) std::string(); // Placement new for std::string
self->data = nullptr; self->data = nullptr;
self->weakreflist = nullptr; // Initialize weakref list
} }
return (PyObject*)self; return (PyObject*)self;
} }
int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) { int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"name", "callback", "interval", NULL}; static const char* kwlist[] = {"name", "callback", "interval", "once", NULL};
const char* name = nullptr; const char* name = nullptr;
PyObject* callback = nullptr; PyObject* callback = nullptr;
int interval = 0; int interval = 0;
int once = 0; // Use int for bool parameter
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi", const_cast<char**>(kwlist), if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi|p", const_cast<char**>(kwlist),
&name, &callback, &interval)) { &name, &callback, &interval, &once)) {
return -1; return -1;
} }
@ -58,8 +76,18 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
} }
// Create the timer callable // Create the timer
self->data = std::make_shared<PyTimerCallable>(callback, interval, current_time); 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 // Register with game engine
if (Resources::game) { if (Resources::game) {
@ -70,6 +98,11 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
} }
void PyTimer::dealloc(PyTimerObject* self) { void PyTimer::dealloc(PyTimerObject* self) {
// Clear weakrefs first
if (self->weakreflist != nullptr) {
PyObject_ClearWeakRefs((PyObject*)self);
}
// Remove from game engine if still registered // Remove from game engine if still registered
if (Resources::game && !self->name.empty()) { if (Resources::game && !self->name.empty()) {
auto it = Resources::game->timers.find(self->name); auto it = Resources::game->timers.find(self->name);
@ -244,7 +277,37 @@ int PyTimer::set_callback(PyTimerObject* self, PyObject* value, void* closure) {
return 0; 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[] = { PyGetSetDef PyTimer::getsetters[] = {
{"name", (getter)PyTimer::get_name, NULL,
"Timer name (read-only)", NULL},
{"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval, {"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval,
"Timer interval in milliseconds", NULL}, "Timer interval in milliseconds", NULL},
{"remaining", (getter)PyTimer::get_remaining, NULL, {"remaining", (getter)PyTimer::get_remaining, NULL,
@ -255,17 +318,27 @@ PyGetSetDef PyTimer::getsetters[] = {
"Whether the timer is active and not paused", NULL}, "Whether the timer is active and not paused", NULL},
{"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback, {"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback,
"The callback function to be called", NULL}, "The callback function to be called", NULL},
{"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once,
"Whether the timer stops after firing once", NULL},
{NULL} {NULL}
}; };
PyMethodDef PyTimer::methods[] = { PyMethodDef PyTimer::methods[] = {
{"pause", (PyCFunction)PyTimer::pause, METH_NOARGS, {"pause", (PyCFunction)PyTimer::pause, METH_NOARGS,
"Pause the timer"}, "pause() -> None\n\n"
"Pause the timer, preserving the time remaining until next trigger.\n"
"The timer can be resumed later with resume()."},
{"resume", (PyCFunction)PyTimer::resume, METH_NOARGS, {"resume", (PyCFunction)PyTimer::resume, METH_NOARGS,
"Resume a paused timer"}, "resume() -> None\n\n"
"Resume a paused timer from where it left off.\n"
"Has no effect if the timer is not paused."},
{"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS, {"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS,
"Cancel the timer and remove it from the system"}, "cancel() -> None\n\n"
"Cancel the timer and remove it from the timer system.\n"
"The timer will no longer fire and cannot be restarted."},
{"restart", (PyCFunction)PyTimer::restart, METH_NOARGS, {"restart", (PyCFunction)PyTimer::restart, METH_NOARGS,
"Restart the timer from the current time"}, "restart() -> None\n\n"
"Restart the timer from the beginning.\n"
"Resets the timer to fire after a full interval from now."},
{NULL} {NULL}
}; };

View File

@ -4,12 +4,13 @@
#include <memory> #include <memory>
#include <string> #include <string>
class PyTimerCallable; class Timer;
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
std::shared_ptr<PyTimerCallable> data; std::shared_ptr<Timer> data;
std::string name; std::string name;
PyObject* weakreflist; // Weak reference support
} PyTimerObject; } PyTimerObject;
class PyTimer class PyTimer
@ -28,6 +29,7 @@ public:
static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
// Timer property getters // Timer property getters
static PyObject* get_name(PyTimerObject* self, void* closure);
static PyObject* get_interval(PyTimerObject* self, void* closure); static PyObject* get_interval(PyTimerObject* self, void* closure);
static int set_interval(PyTimerObject* self, PyObject* value, void* closure); static int set_interval(PyTimerObject* self, PyObject* value, void* closure);
static PyObject* get_remaining(PyTimerObject* self, void* closure); static PyObject* get_remaining(PyTimerObject* self, void* closure);
@ -35,6 +37,8 @@ public:
static PyObject* get_active(PyTimerObject* self, void* closure); static PyObject* get_active(PyTimerObject* self, void* closure);
static PyObject* get_callback(PyTimerObject* self, void* closure); static PyObject* get_callback(PyTimerObject* self, void* closure);
static int set_callback(PyTimerObject* self, PyObject* value, 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 PyGetSetDef getsetters[];
static PyMethodDef methods[]; static PyMethodDef methods[];
@ -49,7 +53,35 @@ namespace mcrfpydef {
.tp_dealloc = (destructor)PyTimer::dealloc, .tp_dealloc = (destructor)PyTimer::dealloc,
.tp_repr = PyTimer::repr, .tp_repr = PyTimer::repr,
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Timer object for scheduled callbacks"), .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_methods = PyTimer::methods,
.tp_getset = PyTimer::getsetters, .tp_getset = PyTimer::getsetters,
.tp_init = (initproc)PyTimer::init, .tp_init = (initproc)PyTimer::init,

85
src/PythonObjectCache.cpp Normal file
View File

@ -0,0 +1,85 @@
#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();
}

40
src/PythonObjectCache.h Normal file
View File

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

View File

@ -1,31 +1,140 @@
#include "Timer.h" #include "Timer.h"
#include "PythonObjectCache.h"
#include "PyCallable.h"
Timer::Timer(PyObject* _target, int _interval, int now) Timer::Timer(PyObject* _target, int _interval, int now, bool _once)
: target(_target), interval(_interval), last_ran(now) : 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() Timer::Timer()
: target(Py_None), interval(0), last_ran(0) : callback(std::make_shared<PyCallable>(Py_None)), interval(0), last_ran(0),
paused(false), pause_start_time(0), total_paused_time(0), once(false)
{} {}
Timer::~Timer() {
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
}
bool Timer::hasElapsed(int now) const
{
if (paused) return false;
return now >= last_ran + interval;
}
bool Timer::test(int now) bool Timer::test(int now)
{ {
if (!target || target == Py_None) return false; if (!callback || callback->isNone()) return false;
if (now > last_ran + interval)
if (hasElapsed(now))
{ {
last_ran = now; last_ran = now;
PyObject* args = Py_BuildValue("(i)", now);
PyObject* retval = PyObject_Call(target, args, NULL); // 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);
if (!retval) if (!retval)
{ {
std::cout << "timer has raised an exception. It's going to STDERR and being dropped:" << std::endl; std::cout << "Timer callback has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print(); PyErr_Print();
PyErr_Clear(); PyErr_Clear();
} else if (retval != Py_None) } else if (retval != Py_None)
{ {
std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; std::cout << "Timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
Py_DECREF(retval);
} }
// Handle one-shot timers
if (once) {
cancel();
}
return true; return true;
} }
return false; return false;
} }
void Timer::pause(int current_time)
{
if (!paused) {
paused = true;
pause_start_time = current_time;
}
}
void Timer::resume(int current_time)
{
if (paused) {
paused = false;
int paused_duration = current_time - pause_start_time;
total_paused_time += paused_duration;
// Adjust last_ran to account for the pause
last_ran += paused_duration;
}
}
void Timer::restart(int current_time)
{
last_ran = current_time;
paused = false;
pause_start_time = 0;
total_paused_time = 0;
}
void Timer::cancel()
{
// Cancel by setting callback to None
callback = std::make_shared<PyCallable>(Py_None);
}
bool Timer::isActive() const
{
return callback && !callback->isNone() && !paused;
}
int Timer::getRemaining(int current_time) const
{
if (paused) {
// When paused, calculate time remaining from when it was paused
int elapsed_when_paused = pause_start_time - last_ran;
return interval - elapsed_when_paused;
}
int elapsed = current_time - last_ran;
return interval - elapsed;
}
int Timer::getElapsed(int current_time) const
{
if (paused) {
return pause_start_time - last_ran;
}
return current_time - last_ran;
}
PyObject* Timer::getCallback()
{
if (!callback) return Py_None;
return callback->borrow();
}
void Timer::setCallback(PyObject* new_callback)
{
callback = std::make_shared<PyCallable>(new_callback);
}

View File

@ -1,15 +1,54 @@
#pragma once #pragma once
#include "Common.h" #include "Common.h"
#include "Python.h" #include "Python.h"
#include <memory>
class PyCallable;
class GameEngine; // forward declare class GameEngine; // forward declare
class Timer class Timer
{ {
public: private:
PyObject* target; std::shared_ptr<PyCallable> callback;
int interval; int interval;
int last_ran; int last_ran;
// Pause/resume support
bool paused;
int pause_start_time;
int total_paused_time;
// One-shot timer support
bool once;
public:
uint64_t serial_number = 0; // For Python object cache
Timer(); // for map to build Timer(); // for map to build
Timer(PyObject*, int, int); Timer(PyObject* target, int interval, int now, bool once = false);
bool test(int); ~Timer();
// Core timer functionality
bool test(int now);
bool hasElapsed(int now) const;
// Timer control methods
void pause(int current_time);
void resume(int current_time);
void restart(int current_time);
void cancel();
// Timer state queries
bool isPaused() const { return paused; }
bool isActive() const;
int getInterval() const { return interval; }
void setInterval(int new_interval) { interval = new_interval; }
int getRemaining(int current_time) const;
int getElapsed(int current_time) const;
bool isOnce() const { return once; }
void setOnce(bool value) { once = value; }
// Callback management
PyObject* getCallback();
void setCallback(PyObject* new_callback);
}; };

View File

@ -6,12 +6,14 @@ class UIEntity;
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
std::shared_ptr<UIEntity> data; std::shared_ptr<UIEntity> data;
PyObject* weakreflist; // Weak reference support
} PyUIEntityObject; } PyUIEntityObject;
class UIFrame; class UIFrame;
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
std::shared_ptr<UIFrame> data; std::shared_ptr<UIFrame> data;
PyObject* weakreflist; // Weak reference support
} PyUIFrameObject; } PyUIFrameObject;
class UICaption; class UICaption;
@ -19,18 +21,21 @@ typedef struct {
PyObject_HEAD PyObject_HEAD
std::shared_ptr<UICaption> data; std::shared_ptr<UICaption> data;
PyObject* font; PyObject* font;
PyObject* weakreflist; // Weak reference support
} PyUICaptionObject; } PyUICaptionObject;
class UIGrid; class UIGrid;
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
std::shared_ptr<UIGrid> data; std::shared_ptr<UIGrid> data;
PyObject* weakreflist; // Weak reference support
} PyUIGridObject; } PyUIGridObject;
class UISprite; class UISprite;
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
std::shared_ptr<UISprite> data; std::shared_ptr<UISprite> data;
PyObject* weakreflist; // Weak reference support
} PyUISpriteObject; } PyUISpriteObject;
// Common Python method implementations for UIDrawable-derived classes // Common Python method implementations for UIDrawable-derived classes

View File

@ -3,6 +3,7 @@
#include "PyColor.h" #include "PyColor.h"
#include "PyVector.h" #include "PyVector.h"
#include "PyFont.h" #include "PyFont.h"
#include "PythonObjectCache.h"
// UIDrawable methods now in UIBase.h // UIDrawable methods now in UIBase.h
#include <algorithm> #include <algorithm>
@ -439,6 +440,19 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
self->data->click_register(click_handler); self->data->click_register(click_handler);
} }
// Initialize weak reference list
self->weakreflist = NULL;
// 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
}
}
return 0; return 0;
} }

View File

@ -54,6 +54,10 @@ namespace mcrfpydef {
.tp_dealloc = (destructor)[](PyObject* self) .tp_dealloc = (destructor)[](PyObject* self)
{ {
PyUICaptionObject* obj = (PyUICaptionObject*)self; PyUICaptionObject* obj = (PyUICaptionObject*)self;
// Clear weak references
if (obj->weakreflist != NULL) {
PyObject_ClearWeakRefs(self);
}
// TODO - reevaluate with PyFont usage; UICaption does not own the font // TODO - reevaluate with PyFont usage; UICaption does not own the font
// release reference to font object // release reference to font object
if (obj->font) Py_DECREF(obj->font); if (obj->font) Py_DECREF(obj->font);
@ -64,7 +68,7 @@ namespace mcrfpydef {
//.tp_hash = NULL, //.tp_hash = NULL,
//.tp_iter //.tp_iter
//.tp_iternext //.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n" .tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n"
"A text display UI element with customizable font and styling.\n\n" "A text display UI element with customizable font and styling.\n\n"
"Args:\n" "Args:\n"
@ -106,7 +110,11 @@ namespace mcrfpydef {
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{ {
PyUICaptionObject* self = (PyUICaptionObject*)type->tp_alloc(type, 0); PyUICaptionObject* self = (PyUICaptionObject*)type->tp_alloc(type, 0);
if (self) self->data = std::make_shared<UICaption>(); if (self) {
self->data = std::make_shared<UICaption>();
self->font = nullptr;
self->weakreflist = nullptr;
}
return (PyObject*)self; return (PyObject*)self;
} }
}; };

View File

@ -6,6 +6,7 @@
#include "UIGrid.h" #include "UIGrid.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PyObjectUtils.h" #include "PyObjectUtils.h"
#include "PythonObjectCache.h"
#include <climits> #include <climits>
#include <algorithm> #include <algorithm>
@ -17,6 +18,14 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
Py_RETURN_NONE; Py_RETURN_NONE;
} }
// Check cache first
if (drawable->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(drawable->serial_number);
if (cached) {
return cached; // Already INCREF'd by lookup
}
}
PyTypeObject* type = nullptr; PyTypeObject* type = nullptr;
PyObject* obj = nullptr; PyObject* obj = nullptr;
@ -28,6 +37,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0); auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0);
if (pyObj) { if (pyObj) {
pyObj->data = std::static_pointer_cast<UIFrame>(drawable); pyObj->data = std::static_pointer_cast<UIFrame>(drawable);
pyObj->weakreflist = NULL;
} }
obj = (PyObject*)pyObj; obj = (PyObject*)pyObj;
break; break;
@ -40,6 +50,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
if (pyObj) { if (pyObj) {
pyObj->data = std::static_pointer_cast<UICaption>(drawable); pyObj->data = std::static_pointer_cast<UICaption>(drawable);
pyObj->font = nullptr; pyObj->font = nullptr;
pyObj->weakreflist = NULL;
} }
obj = (PyObject*)pyObj; obj = (PyObject*)pyObj;
break; break;
@ -51,6 +62,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0); auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0);
if (pyObj) { if (pyObj) {
pyObj->data = std::static_pointer_cast<UISprite>(drawable); pyObj->data = std::static_pointer_cast<UISprite>(drawable);
pyObj->weakreflist = NULL;
} }
obj = (PyObject*)pyObj; obj = (PyObject*)pyObj;
break; break;
@ -62,6 +74,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
auto pyObj = (PyUIGridObject*)type->tp_alloc(type, 0); auto pyObj = (PyUIGridObject*)type->tp_alloc(type, 0);
if (pyObj) { if (pyObj) {
pyObj->data = std::static_pointer_cast<UIGrid>(drawable); pyObj->data = std::static_pointer_cast<UIGrid>(drawable);
pyObj->weakreflist = NULL;
} }
obj = (PyObject*)pyObj; obj = (PyObject*)pyObj;
break; break;

View File

@ -5,9 +5,113 @@
#include "UIGrid.h" #include "UIGrid.h"
#include "GameEngine.h" #include "GameEngine.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PythonObjectCache.h"
UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; } UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; }
UIDrawable::UIDrawable(const UIDrawable& other)
: z_index(other.z_index),
name(other.name),
position(other.position),
visible(other.visible),
opacity(other.opacity),
serial_number(0), // Don't copy serial number
use_render_texture(other.use_render_texture),
render_dirty(true) // Force redraw after copy
{
// Deep copy click_callable if it exists
if (other.click_callable) {
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
}
// Deep copy render texture if needed
if (other.render_texture && other.use_render_texture) {
auto size = other.render_texture->getSize();
enableRenderTexture(size.x, size.y);
}
}
UIDrawable& UIDrawable::operator=(const UIDrawable& other) {
if (this != &other) {
// Copy basic members
z_index = other.z_index;
name = other.name;
position = other.position;
visible = other.visible;
opacity = other.opacity;
use_render_texture = other.use_render_texture;
render_dirty = true; // Force redraw after copy
// Deep copy click_callable
if (other.click_callable) {
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
} else {
click_callable.reset();
}
// Deep copy render texture if needed
if (other.render_texture && other.use_render_texture) {
auto size = other.render_texture->getSize();
enableRenderTexture(size.x, size.y);
} else {
render_texture.reset();
use_render_texture = false;
}
}
return *this;
}
UIDrawable::UIDrawable(UIDrawable&& other) noexcept
: z_index(other.z_index),
name(std::move(other.name)),
position(other.position),
visible(other.visible),
opacity(other.opacity),
serial_number(other.serial_number),
click_callable(std::move(other.click_callable)),
render_texture(std::move(other.render_texture)),
render_sprite(std::move(other.render_sprite)),
use_render_texture(other.use_render_texture),
render_dirty(other.render_dirty)
{
// Clear the moved-from object's serial number to avoid cache issues
other.serial_number = 0;
}
UIDrawable& UIDrawable::operator=(UIDrawable&& other) noexcept {
if (this != &other) {
// Clear our own cache entry if we have one
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
// Move basic members
z_index = other.z_index;
name = std::move(other.name);
position = other.position;
visible = other.visible;
opacity = other.opacity;
serial_number = other.serial_number;
use_render_texture = other.use_render_texture;
render_dirty = other.render_dirty;
// Move unique_ptr members
click_callable = std::move(other.click_callable);
render_texture = std::move(other.render_texture);
render_sprite = std::move(other.render_sprite);
// Clear the moved-from object's serial number
other.serial_number = 0;
}
return *this;
}
UIDrawable::~UIDrawable() {
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
}
void UIDrawable::click_unregister() void UIDrawable::click_unregister()
{ {
click_callable.reset(); click_callable.reset();

View File

@ -39,6 +39,15 @@ public:
void click_unregister(); void click_unregister();
UIDrawable(); UIDrawable();
virtual ~UIDrawable();
// Copy constructor and assignment operator
UIDrawable(const UIDrawable& other);
UIDrawable& operator=(const UIDrawable& other);
// Move constructor and assignment operator
UIDrawable(UIDrawable&& other) noexcept;
UIDrawable& operator=(UIDrawable&& other) noexcept;
static PyObject* get_click(PyObject* self, void* closure); static PyObject* get_click(PyObject* self, void* closure);
static int set_click(PyObject* self, PyObject* value, void* closure); static int set_click(PyObject* self, PyObject* value, void* closure);
@ -90,6 +99,9 @@ public:
virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; } virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; }
virtual bool getProperty(const std::string& name, std::string& value) const { return false; } virtual bool getProperty(const std::string& name, std::string& value) const { return false; }
// Python object cache support
uint64_t serial_number = 0;
protected: protected:
// RenderTexture support (opt-in) // RenderTexture support (opt-in)
std::unique_ptr<sf::RenderTexture> render_texture; std::unique_ptr<sf::RenderTexture> render_texture;

View File

@ -4,6 +4,7 @@
#include <algorithm> #include <algorithm>
#include "PyObjectUtils.h" #include "PyObjectUtils.h"
#include "PyVector.h" #include "PyVector.h"
#include "PythonObjectCache.h"
// UIDrawable methods now in UIBase.h // UIDrawable methods now in UIBase.h
#include "UIEntityPyMethods.h" #include "UIEntityPyMethods.h"
@ -16,6 +17,12 @@ UIEntity::UIEntity()
// gridstate vector starts empty - will be lazily initialized when needed // gridstate vector starts empty - will be lazily initialized when needed
} }
UIEntity::~UIEntity() {
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
}
// Removed UIEntity(UIGrid&) constructor - using lazy initialization instead // Removed UIEntity(UIGrid&) constructor - using lazy initialization instead
void UIEntity::updateVisibility() void UIEntity::updateVisibility()
@ -186,8 +193,21 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
// Create the entity // Create the entity
self->data = std::make_shared<UIEntity>(); self->data = std::make_shared<UIEntity>();
// Initialize weak reference list
self->weakreflist = NULL;
// Store reference to Python object // 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
}
}
// Store reference to Python object (legacy - to be removed)
self->data->self = (PyObject*)self; self->data->self = (PyObject*)self;
Py_INCREF(self); Py_INCREF(self);

View File

@ -14,12 +14,37 @@
#include "PyFont.h" #include "PyFont.h"
#include "UIGridPoint.h" #include "UIGridPoint.h"
#include "UIDrawable.h"
#include "UIBase.h" #include "UIBase.h"
#include "UISprite.h" #include "UISprite.h"
class UIGrid; class UIGrid;
// UIEntity
/*
****************************************
* say it with me: *
* UIEntity is not a UIDrawable *
****************************************
**Why Not, John?**
Doesn't it say "UI" on the front of it?
It sure does. Probably should have called it "GridEntity", but it's a bit late now.
UIDrawables have positions in **screen pixel coordinates**. Their position is an offset from their parent's position, and they are deeply nestable (Scene -> Frame -> Frame -> ...)
However:
UIEntity has a position in **Grid tile coordinates**.
UIEntity is not nestable at all. Grid -> Entity.
UIEntity has a strict one/none relationship with a Grid: if you add it to another grid, it will have itself removed from the losing grid's collection.
UIEntity originally only allowed a single-tile sprite, but around mid-July 2025, I'm working to change that to allow any UIDrawable to go there, or multi-tile sprites.
UIEntity is, at its core, the container for *a perspective of map data*.
The Grid should contain the true, complete contents of the game's space, and the Entity should use pathfinding, field of view, and game logic to interact with the Grid's layer data.
In Conclusion, because UIEntity cannot be drawn on a Frame or Scene, and has the unique role of serving as a filter of the data contained in a Grid, UIEntity will not become a UIDrawable.
*/
//class UIEntity; //class UIEntity;
//typedef struct { //typedef struct {
// PyObject_HEAD // PyObject_HEAD
@ -32,11 +57,11 @@ sf::Vector2f PyObject_to_sfVector2f(PyObject* obj);
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state); PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state);
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec); PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec);
// TODO: make UIEntity a drawable class UIEntity
class UIEntity//: public UIDrawable
{ {
public: public:
PyObject* self = nullptr; // Reference to the Python object (if created from Python) PyObject* self = nullptr; // Reference to the Python object (if created from Python)
uint64_t serial_number = 0; // For Python object cache
std::shared_ptr<UIGrid> grid; std::shared_ptr<UIGrid> grid;
std::vector<UIGridPointState> gridstate; std::vector<UIGridPointState> gridstate;
UISprite sprite; UISprite sprite;
@ -44,6 +69,7 @@ public:
//void render(sf::Vector2f); //override final; //void render(sf::Vector2f); //override final;
UIEntity(); UIEntity();
~UIEntity();
// Visibility methods // Visibility methods
void updateVisibility(); // Update gridstate from current FOV void updateVisibility(); // Update gridstate from current FOV
@ -112,7 +138,7 @@ namespace mcrfpydef {
" name (str): Element name"), " name (str): Element name"),
.tp_methods = UIEntity_all_methods, .tp_methods = UIEntity_all_methods,
.tp_getset = UIEntity::getsetters, .tp_getset = UIEntity::getsetters,
.tp_base = &mcrfpydef::PyDrawableType, .tp_base = NULL,
.tp_init = (initproc)UIEntity::init, .tp_init = (initproc)UIEntity::init,
.tp_new = PyType_GenericNew, .tp_new = PyType_GenericNew,
}; };

View File

@ -6,6 +6,7 @@
#include "UISprite.h" #include "UISprite.h"
#include "UIGrid.h" #include "UIGrid.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PythonObjectCache.h"
// UIDrawable methods now in UIBase.h // UIDrawable methods now in UIBase.h
UIDrawable* UIFrame::click_at(sf::Vector2f point) UIDrawable* UIFrame::click_at(sf::Vector2f point)
@ -431,6 +432,9 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
// Initialize children first // Initialize children first
self->data->children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>(); self->data->children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
// Initialize weak reference list
self->weakreflist = NULL;
// Define all parameters with defaults // Define all parameters with defaults
PyObject* pos_obj = nullptr; PyObject* pos_obj = nullptr;
PyObject* size_obj = nullptr; PyObject* size_obj = nullptr;
@ -624,6 +628,16 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
self->data->click_register(click_handler); self->data->click_register(click_handler);
} }
// 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
}
}
return 0; return 0;
} }

View File

@ -78,6 +78,10 @@ namespace mcrfpydef {
.tp_dealloc = (destructor)[](PyObject* self) .tp_dealloc = (destructor)[](PyObject* self)
{ {
PyUIFrameObject* obj = (PyUIFrameObject*)self; PyUIFrameObject* obj = (PyUIFrameObject*)self;
// Clear weak references
if (obj->weakreflist != NULL) {
PyObject_ClearWeakRefs(self);
}
obj->data.reset(); obj->data.reset();
Py_TYPE(self)->tp_free(self); Py_TYPE(self)->tp_free(self);
}, },
@ -85,7 +89,7 @@ namespace mcrfpydef {
//.tp_hash = NULL, //.tp_hash = NULL,
//.tp_iter //.tp_iter
//.tp_iternext //.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n" .tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n"
"A rectangular frame UI element that can contain other drawable elements.\n\n" "A rectangular frame UI element that can contain other drawable elements.\n\n"
"Args:\n" "Args:\n"
@ -127,7 +131,10 @@ namespace mcrfpydef {
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{ {
PyUIFrameObject* self = (PyUIFrameObject*)type->tp_alloc(type, 0); PyUIFrameObject* self = (PyUIFrameObject*)type->tp_alloc(type, 0);
if (self) self->data = std::make_shared<UIFrame>(); if (self) {
self->data = std::make_shared<UIFrame>();
self->weakreflist = nullptr;
}
return (PyObject*)self; return (PyObject*)self;
} }
}; };

View File

@ -1,6 +1,7 @@
#include "UIGrid.h" #include "UIGrid.h"
#include "GameEngine.h" #include "GameEngine.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "PythonObjectCache.h"
#include <algorithm> #include <algorithm>
// UIDrawable methods now in UIBase.h // UIDrawable methods now in UIBase.h
@ -680,6 +681,19 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
self->data->click_register(click_handler); self->data->click_register(click_handler);
} }
// Initialize weak reference list
self->weakreflist = NULL;
// 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
}
}
return 0; // Success return 0; // Success
} }
@ -1394,7 +1408,15 @@ PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize
std::advance(l_begin, index); std::advance(l_begin, index);
auto target = *l_begin; //auto target = (*vec)[index]; auto target = *l_begin; //auto target = (*vec)[index];
// If the entity has a stored Python object reference, return that to preserve derived class // Check cache first to preserve derived class
if (target->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(target->serial_number);
if (cached) {
return cached; // Already INCREF'd by lookup
}
}
// Legacy: If the entity has a stored Python object reference, return that to preserve derived class
if (target->self != nullptr) { if (target->self != nullptr) {
Py_INCREF(target->self); Py_INCREF(target->self);
return target->self; return target->self;

View File

@ -172,18 +172,22 @@ namespace mcrfpydef {
.tp_name = "mcrfpy.Grid", .tp_name = "mcrfpy.Grid",
.tp_basicsize = sizeof(PyUIGridObject), .tp_basicsize = sizeof(PyUIGridObject),
.tp_itemsize = 0, .tp_itemsize = 0,
//.tp_dealloc = (destructor)[](PyObject* self) .tp_dealloc = (destructor)[](PyObject* self)
//{ {
// PyUIGridObject* obj = (PyUIGridObject*)self; PyUIGridObject* obj = (PyUIGridObject*)self;
// obj->data.reset(); // Clear weak references
// Py_TYPE(self)->tp_free(self); if (obj->weakreflist != NULL) {
//}, PyObject_ClearWeakRefs(self);
}
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
//TODO - PyUIGrid REPR def: //TODO - PyUIGrid REPR def:
.tp_repr = (reprfunc)UIGrid::repr, .tp_repr = (reprfunc)UIGrid::repr,
//.tp_hash = NULL, //.tp_hash = NULL,
//.tp_iter //.tp_iter
//.tp_iternext //.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n" .tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n"
"A grid-based UI element for tile-based rendering and entity management.\n\n" "A grid-based UI element for tile-based rendering and entity management.\n\n"
"Args:\n" "Args:\n"

View File

@ -1,6 +1,7 @@
#include "UISprite.h" #include "UISprite.h"
#include "GameEngine.h" #include "GameEngine.h"
#include "PyVector.h" #include "PyVector.h"
#include "PythonObjectCache.h"
// UIDrawable methods now in UIBase.h // UIDrawable methods now in UIBase.h
UIDrawable* UISprite::click_at(sf::Vector2f point) UIDrawable* UISprite::click_at(sf::Vector2f point)
@ -28,6 +29,42 @@ UISprite::UISprite(std::shared_ptr<PyTexture> _ptex, int _sprite_index, sf::Vect
sprite = ptex->sprite(sprite_index, position, sf::Vector2f(_scale, _scale)); sprite = ptex->sprite(sprite_index, position, sf::Vector2f(_scale, _scale));
} }
UISprite::UISprite(const UISprite& other)
: UIDrawable(other),
sprite_index(other.sprite_index),
sprite(other.sprite),
ptex(other.ptex)
{
}
UISprite& UISprite::operator=(const UISprite& other) {
if (this != &other) {
UIDrawable::operator=(other);
sprite_index = other.sprite_index;
sprite = other.sprite;
ptex = other.ptex;
}
return *this;
}
UISprite::UISprite(UISprite&& other) noexcept
: UIDrawable(std::move(other)),
sprite_index(other.sprite_index),
sprite(std::move(other.sprite)),
ptex(std::move(other.ptex))
{
}
UISprite& UISprite::operator=(UISprite&& other) noexcept {
if (this != &other) {
UIDrawable::operator=(std::move(other));
sprite_index = other.sprite_index;
sprite = std::move(other.sprite);
ptex = std::move(other.ptex);
}
return *this;
}
/* /*
void UISprite::render(sf::Vector2f offset) void UISprite::render(sf::Vector2f offset)
{ {
@ -432,6 +469,19 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
self->data->click_register(click_handler); self->data->click_register(click_handler);
} }
// Initialize weak reference list
self->weakreflist = NULL;
// 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
}
}
return 0; return 0;
} }

View File

@ -25,6 +25,14 @@ protected:
public: public:
UISprite(); UISprite();
UISprite(std::shared_ptr<PyTexture>, int, sf::Vector2f, float); UISprite(std::shared_ptr<PyTexture>, int, sf::Vector2f, float);
// Copy constructor and assignment operator
UISprite(const UISprite& other);
UISprite& operator=(const UISprite& other);
// Move constructor and assignment operator
UISprite(UISprite&& other) noexcept;
UISprite& operator=(UISprite&& other) noexcept;
void update(); void update();
void render(sf::Vector2f, sf::RenderTarget&) override final; void render(sf::Vector2f, sf::RenderTarget&) override final;
virtual UIDrawable* click_at(sf::Vector2f point) override final; virtual UIDrawable* click_at(sf::Vector2f point) override final;
@ -82,6 +90,10 @@ namespace mcrfpydef {
.tp_dealloc = (destructor)[](PyObject* self) .tp_dealloc = (destructor)[](PyObject* self)
{ {
PyUISpriteObject* obj = (PyUISpriteObject*)self; PyUISpriteObject* obj = (PyUISpriteObject*)self;
// Clear weak references
if (obj->weakreflist != NULL) {
PyObject_ClearWeakRefs(self);
}
// release reference to font object // release reference to font object
//if (obj->texture) Py_DECREF(obj->texture); //if (obj->texture) Py_DECREF(obj->texture);
obj->data.reset(); obj->data.reset();
@ -91,7 +103,7 @@ namespace mcrfpydef {
//.tp_hash = NULL, //.tp_hash = NULL,
//.tp_iter //.tp_iter
//.tp_iternext //.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n" .tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
"A sprite UI element that displays a texture or portion of a texture atlas.\n\n" "A sprite UI element that displays a texture or portion of a texture atlas.\n\n"
"Args:\n" "Args:\n"
@ -130,7 +142,10 @@ namespace mcrfpydef {
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{ {
PyUISpriteObject* self = (PyUISpriteObject*)type->tp_alloc(type, 0); PyUISpriteObject* self = (PyUISpriteObject*)type->tp_alloc(type, 0);
//if (self) self->data = std::make_shared<UICaption>(); if (self) {
self->data = std::make_shared<UISprite>();
self->weakreflist = nullptr;
}
return (PyObject*)self; return (PyObject*)self;
} }
}; };

215
tests/constructor_audit.py Normal file
View File

@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Audit current constructor argument handling for all UI classes"""
import mcrfpy
import sys
def audit_constructors():
"""Test current state of all UI constructors"""
print("=== CONSTRUCTOR AUDIT ===\n")
# Create test scene and texture
mcrfpy.createScene("audit")
texture = mcrfpy.Texture("assets/test_portraits.png", 32, 32)
# Test Frame
print("1. Frame Constructor Tests:")
print("-" * 40)
# No args
try:
f = mcrfpy.Frame()
print("✓ Frame() - works")
except Exception as e:
print(f"✗ Frame() - {e}")
# Traditional 4 args (x, y, w, h)
try:
f = mcrfpy.Frame(10, 20, 100, 50)
print("✓ Frame(10, 20, 100, 50) - works")
except Exception as e:
print(f"✗ Frame(10, 20, 100, 50) - {e}")
# Tuple pos + size
try:
f = mcrfpy.Frame((10, 20), (100, 50))
print("✓ Frame((10, 20), (100, 50)) - works")
except Exception as e:
print(f"✗ Frame((10, 20), (100, 50)) - {e}")
# Keywords
try:
f = mcrfpy.Frame(pos=(10, 20), size=(100, 50))
print("✓ Frame(pos=(10, 20), size=(100, 50)) - works")
except Exception as e:
print(f"✗ Frame(pos=(10, 20), size=(100, 50)) - {e}")
# Test Grid
print("\n2. Grid Constructor Tests:")
print("-" * 40)
# No args
try:
g = mcrfpy.Grid()
print("✓ Grid() - works")
except Exception as e:
print(f"✗ Grid() - {e}")
# Grid size only
try:
g = mcrfpy.Grid((10, 10))
print("✓ Grid((10, 10)) - works")
except Exception as e:
print(f"✗ Grid((10, 10)) - {e}")
# Grid size + texture
try:
g = mcrfpy.Grid((10, 10), texture)
print("✓ Grid((10, 10), texture) - works")
except Exception as e:
print(f"✗ Grid((10, 10), texture) - {e}")
# Full positional (expected: pos, size, grid_size, texture)
try:
g = mcrfpy.Grid((0, 0), (320, 320), (10, 10), texture)
print("✓ Grid((0, 0), (320, 320), (10, 10), texture) - works")
except Exception as e:
print(f"✗ Grid((0, 0), (320, 320), (10, 10), texture) - {e}")
# Keywords
try:
g = mcrfpy.Grid(pos=(0, 0), size=(320, 320), grid_size=(10, 10), texture=texture)
print("✓ Grid(pos=..., size=..., grid_size=..., texture=...) - works")
except Exception as e:
print(f"✗ Grid(pos=..., size=..., grid_size=..., texture=...) - {e}")
# Test Sprite
print("\n3. Sprite Constructor Tests:")
print("-" * 40)
# No args
try:
s = mcrfpy.Sprite()
print("✓ Sprite() - works")
except Exception as e:
print(f"✗ Sprite() - {e}")
# Position only
try:
s = mcrfpy.Sprite((10, 20))
print("✓ Sprite((10, 20)) - works")
except Exception as e:
print(f"✗ Sprite((10, 20)) - {e}")
# Position + texture
try:
s = mcrfpy.Sprite((10, 20), texture)
print("✓ Sprite((10, 20), texture) - works")
except Exception as e:
print(f"✗ Sprite((10, 20), texture) - {e}")
# Position + texture + sprite_index
try:
s = mcrfpy.Sprite((10, 20), texture, 5)
print("✓ Sprite((10, 20), texture, 5) - works")
except Exception as e:
print(f"✗ Sprite((10, 20), texture, 5) - {e}")
# Keywords
try:
s = mcrfpy.Sprite(pos=(10, 20), texture=texture, sprite_index=5)
print("✓ Sprite(pos=..., texture=..., sprite_index=...) - works")
except Exception as e:
print(f"✗ Sprite(pos=..., texture=..., sprite_index=...) - {e}")
# Test Caption
print("\n4. Caption Constructor Tests:")
print("-" * 40)
# No args
try:
c = mcrfpy.Caption()
print("✓ Caption() - works")
except Exception as e:
print(f"✗ Caption() - {e}")
# Text only
try:
c = mcrfpy.Caption("Hello")
print("✓ Caption('Hello') - works")
except Exception as e:
print(f"✗ Caption('Hello') - {e}")
# Position + text (expected order: pos, font, text)
try:
c = mcrfpy.Caption((10, 20), "Hello")
print("✓ Caption((10, 20), 'Hello') - works")
except Exception as e:
print(f"✗ Caption((10, 20), 'Hello') - {e}")
# Position + font + text
try:
c = mcrfpy.Caption((10, 20), 16, "Hello")
print("✓ Caption((10, 20), 16, 'Hello') - works")
except Exception as e:
print(f"✗ Caption((10, 20), 16, 'Hello') - {e}")
# Keywords
try:
c = mcrfpy.Caption(pos=(10, 20), font=16, text="Hello")
print("✓ Caption(pos=..., font=..., text=...) - works")
except Exception as e:
print(f"✗ Caption(pos=..., font=..., text=...) - {e}")
# Test Entity
print("\n5. Entity Constructor Tests:")
print("-" * 40)
# No args
try:
e = mcrfpy.Entity()
print("✓ Entity() - works")
except Exception as e:
print(f"✗ Entity() - {e}")
# Grid position only
try:
e = mcrfpy.Entity((5.0, 6.0))
print("✓ Entity((5.0, 6.0)) - works")
except Exception as e:
print(f"✗ Entity((5.0, 6.0)) - {e}")
# Grid position + texture
try:
e = mcrfpy.Entity((5.0, 6.0), texture)
print("✓ Entity((5.0, 6.0), texture) - works")
except Exception as e:
print(f"✗ Entity((5.0, 6.0), texture) - {e}")
# Grid position + texture + sprite_index
try:
e = mcrfpy.Entity((5.0, 6.0), texture, 3)
print("✓ Entity((5.0, 6.0), texture, 3) - works")
except Exception as e:
print(f"✗ Entity((5.0, 6.0), texture, 3) - {e}")
# Keywords
try:
e = mcrfpy.Entity(grid_pos=(5.0, 6.0), texture=texture, sprite_index=3)
print("✓ Entity(grid_pos=..., texture=..., sprite_index=...) - works")
except Exception as e:
print(f"✗ Entity(grid_pos=..., texture=..., sprite_index=...) - {e}")
print("\n=== AUDIT COMPLETE ===")
# Run audit
try:
audit_constructors()
print("\nPASS")
sys.exit(0)
except Exception as e:
print(f"\nFAIL: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
# Count format string characters
fmt = "|OOOOfOOifizfffi"
print(f"Format string: {fmt}")
# Remove the | prefix
fmt_chars = fmt[1:]
print(f"Format chars after |: {fmt_chars}")
print(f"Length: {len(fmt_chars)}")
# Count each type
o_count = fmt_chars.count('O')
f_count = fmt_chars.count('f')
i_count = fmt_chars.count('i')
z_count = fmt_chars.count('z')
s_count = fmt_chars.count('s')
print(f"\nCounts:")
print(f"O (objects): {o_count}")
print(f"f (floats): {f_count}")
print(f"i (ints): {i_count}")
print(f"z (strings): {z_count}")
print(f"s (strings): {s_count}")
print(f"Total: {o_count + f_count + i_count + z_count + s_count}")
# List out each position
print("\nPosition by position:")
for i, c in enumerate(fmt_chars):
print(f"{i+1}: {c}")

View File

@ -1,42 +0,0 @@
#!/bin/bash
# Run all tests and check for failures
TESTS=(
"test_click_init.py"
"test_drawable_base.py"
"test_frame_children.py"
"test_sprite_texture_swap.py"
"test_timer_object.py"
"test_timer_object_fixed.py"
)
echo "Running all tests..."
echo "===================="
failed=0
passed=0
for test in "${TESTS[@]}"; do
echo -n "Running $test... "
if timeout 5 ./mcrogueface --headless --exec ../tests/$test > /tmp/test_output.txt 2>&1; then
if grep -q "FAIL\|✗" /tmp/test_output.txt; then
echo "FAILED"
echo "Output:"
cat /tmp/test_output.txt | grep -E "✗|FAIL|Error|error" | head -10
((failed++))
else
echo "PASSED"
((passed++))
fi
else
echo "TIMEOUT/CRASH"
((failed++))
fi
done
echo "===================="
echo "Total: $((passed + failed)) tests"
echo "Passed: $passed"
echo "Failed: $failed"
exit $failed

View File

@ -1,102 +0,0 @@
import json
from time import time
#with open("/home/john/issues.json", "r") as f:
# data = json.loads(f.read())
#with open("/home/john/issues2.json", "r") as f:
# data.extend(json.loads(f.read()))
print("Fetching issues...", end='')
start = time()
from gitea import Gitea, Repository, Issue
g = Gitea("https://gamedev.ffwf.net/gitea", token_text="3b450f66e21d62c22bb9fa1c8b975049a5d0c38d")
repo = Repository.request(g, "john", "McRogueFace")
issues = repo.get_issues()
dur = time() - start
print(f"({dur:.1f}s)")
print("Gitea Version: " + g.get_version())
print("API-Token belongs to user: " + g.get_user().username)
data = [
{
"labels": i.labels,
"body": i.body,
"number": i.number,
}
for i in issues
]
input()
def front_number(txt):
if not txt[0].isdigit(): return None
number = ""
for c in txt:
if not c.isdigit():
break
number += c
return int(number)
def split_any(txt, splitters):
tokens = []
txt = [txt]
for s in splitters:
for t in txt:
tokens.extend(t.split(s))
txt = tokens
tokens = []
return txt
def find_refs(txt):
tokens = [tok for tok in split_any(txt, ' ,;\t\r\n') if tok.startswith('#')]
return [front_number(tok[1:]) for tok in tokens]
from collections import defaultdict
issue_relations = defaultdict(list)
nodes = set()
for issue in data:
#refs = issue['body'].split('#')[1::2]
#refs = [front_number(r) for r in refs if front_number(r) is not None]
refs = find_refs(issue['body'])
print(issue['number'], ':', refs)
issue_relations[issue['number']].extend(refs)
nodes.add(issue['number'])
for r in refs:
nodes.add(r)
issue_relations[r].append(issue['number'])
# Find issue labels
issue_labels = {}
for d in data:
labels = [l['name'] for l in d['labels']]
#print(d['number'], labels)
issue_labels[d['number']] = labels
import networkx as nx
import matplotlib.pyplot as plt
relations = nx.Graph()
for k in issue_relations:
relations.add_node(k)
for r in issue_relations[k]:
relations.add_edge(k, r)
relations.add_edge(r, k)
#nx.draw_networkx(relations)
pos = nx.spring_layout(relations)
nx.draw_networkx_nodes(relations, pos,
nodelist = [n for n in issue_labels if 'Alpha Release Requirement' in issue_labels[n]],
node_color="tab:red")
nx.draw_networkx_nodes(relations, pos,
nodelist = [n for n in issue_labels if 'Alpha Release Requirement' not in issue_labels[n]],
node_color="tab:blue")
nx.draw_networkx_edges(relations, pos,
edgelist = relations.edges()
)
nx.draw_networkx_labels(relations, pos, {i: str(i) for i in relations.nodes()})
plt.show()