Compare commits

..

5 Commits

Author SHA1 Message Date
John McCardle 99f301e3a0 Add position tuple support and pos property to UI elements
closes #83, closes #84

- Issue #83: Add position tuple support to constructors
  - Frame and Sprite now accept both (x, y) and ((x, y)) forms
  - Also accept Vector objects as position arguments
  - Caption and Entity already supported tuple/Vector forms
  - Uses PyVector::from_arg for flexible position parsing

- Issue #84: Add pos property to Frame and Sprite
  - Added pos getter that returns a Vector
  - Added pos setter that accepts Vector or tuple
  - Provides consistency with Caption and Entity which already had pos properties
  - All UI elements now have a uniform way to get/set positions as Vectors

Both features improve API consistency and make it easier to work with positions.
2025-07-05 16:25:32 -04:00
John McCardle 2f2b488fb5 Standardize sprite_index property and add scale_x/scale_y to UISprite
closes #81, closes #82

- Issue #81: Standardized property name to sprite_index across UISprite and UIEntity
  - Added sprite_index as the primary property name
  - Kept sprite_number as a deprecated alias for backward compatibility
  - Updated repr() methods to use sprite_index
  - Updated animation system to recognize both names

- Issue #82: Added scale_x and scale_y properties to UISprite
  - Enables non-uniform scaling of sprites
  - scale property still works for uniform scaling
  - Both properties work with the animation system

All existing code using sprite_number continues to work due to backward compatibility.
2025-07-05 16:18:10 -04:00
John McCardle 5a003a9aa5 Fix multiple low priority issues
closes #12, closes #80, closes #95, closes #96, closes #99

- Issue #12: Set tp_new to NULL for GridPoint and GridPointState to prevent instantiation from Python
- Issue #80: Renamed Caption.size to Caption.font_size for semantic clarity
- Issue #95: Fixed UICollection repr to show actual derived types instead of generic UIDrawable
- Issue #96: Added extend() method to UICollection for API consistency with UIEntityCollection
- Issue #99: Exposed read-only properties for Texture (sprite_width, sprite_height, sheet_width, sheet_height, sprite_count, source) and Font (family, source)

All issues have corresponding tests that verify the fixes work correctly.
2025-07-05 16:09:52 -04:00
John McCardle e5affaf317 Fix critical issues: script loading, entity types, and color properties
- Issue #37: Fix Windows scripts subdirectory not checked
  - Updated executeScript() to use executable_path() from platform.h
  - Scripts now load correctly when working directory differs from executable

- Issue #76: Fix UIEntityCollection returns wrong type
  - Updated UIEntityCollectionIter::next() to check for stored Python object
  - Derived Entity classes now preserve their type when retrieved from collections

- Issue #9: Recreate RenderTexture when resized (already fixed)
  - Confirmed RenderTexture recreation already implemented in set_size() and set_float_member()
  - Uses 1.5x padding and 4096 max size limit

- Issue #79: Fix Color r, g, b, a properties return None
  - Implemented get_member() and set_member() in PyColor.cpp
  - Color component properties now work correctly with proper validation

- Additional fix: Grid.at() method signature
  - Changed from METH_O to METH_VARARGS to accept two arguments

All fixes include comprehensive tests to verify functionality.

closes #37, closes #76, closes #9, closes #79

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 15:50:09 -04:00
John McCardle d03182d347 Squashed commit of the following: [interpreter_mode]
closes #63
closes #69
closes #59
closes #47
closes #2
closes #3
closes #33
closes #27
closes #73
closes #74
closes #78

  I'd like to thank Claude Code for ~200-250M total tokens and 5-7M output tokens

    🤖 Generated with [Claude Code](https://claude.ai/code)
    Co-Authored-By: Claude <noreply@anthropic.com>

commit 9bd1561bfc
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 11:20:07 2025 -0400

    Alpha 0.1 release
    - Move RenderTexture (#6) out of alpha requirements, I don't need it
      that badly
    - alpha blockers resolved:
      * Animation system (#59)
      * Z-order rendering (#63)
      * Python Sequence Protocol (#69)
      * New README (#47)
      * Removed deprecated methods (#2, #3)

    🍾 McRogueFace 0.1.0

commit 43321487eb
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 10:36:09 2025 -0400

    Issue #63 (z-order rendering) complete
    - Archive z-order test files

commit 90c318104b
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 10:34:06 2025 -0400

    Fix Issue #63: Implement z-order rendering with dirty flag optimization

    - Add dirty flags to PyScene and UIFrame to track when sorting is needed
    - Implement lazy sorting - only sort when z_index changes or elements are added/removed
    - Make Frame children respect z_index (previously rendered in insertion order only)
    - Update UIDrawable::set_int to notify when z_index changes
    - Mark collections dirty on append, remove, setitem, and slice operations
    - Remove per-frame vector copy in PyScene::render for better performance

commit e4482e7189
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 01:58:03 2025 -0400

    Implement complete Python Sequence Protocol for collections (closes #69)

    Major implementation of the full sequence protocol for both UICollection
    and UIEntityCollection, making them behave like proper Python sequences.

    Core Features Implemented:
    - __setitem__ (collection[i] = value) with type validation
    - __delitem__ (del collection[i]) with proper cleanup
    - __contains__ (item in collection) by C++ pointer comparison
    - __add__ (collection + other) returns Python list
    - __iadd__ (collection += other) with full validation before modification
    - Negative indexing support throughout
    - Complete slice support (getting, setting, deletion)
    - Extended slices with step \!= 1
    - index() and count() methods
    - Type safety enforced for all operations

    UICollection specifics:
    - Accepts Frame, Caption, Sprite, and Grid objects only
    - Preserves z_index when replacing items
    - Auto-assigns z_index on append (existing behavior maintained)

    UIEntityCollection specifics:
    - Accepts Entity objects only
    - Manages grid references on add/remove/replace
    - Uses std::list iteration with std::advance()

    Also includes:
    - Default value support for constructors:
      - Caption accepts None for font (uses default_font)
      - Grid accepts None for texture (uses default_texture)
      - Sprite accepts None for texture (uses default_texture)
      - Entity accepts None for texture (uses default_texture)

    This completes Issue #69, removing it as an Alpha Blocker.

commit 70cf44f8f0
Author: John McCardle <mccardle.john@gmail.com>
Date:   Sat Jul 5 00:56:42 2025 -0400

    Implement comprehensive animation system (closes #59)

    - Add Animation class with 30+ easing functions (linear, ease in/out, quad, cubic, elastic, bounce, etc.)
    - Add property system to all UI classes for animation support:
      - UIFrame: position, size, colors (including individual r/g/b/a components)
      - UICaption: position, size, text, colors
      - UISprite: position, scale, sprite_number (with sequence support)
      - UIGrid: position, size, camera center, zoom
      - UIEntity: position, sprite properties
    - Create AnimationManager singleton for frame-based updates
    - Add Python bindings through PyAnimation wrapper
    - Support for delta animations (relative values)
    - Fix segfault when running scripts directly (mcrf_module initialization)
    - Fix headless/windowed mode behavior to respect --headless flag
    - Animations run purely in C++ without Python callbacks per frame

    All UI properties are now animatable with smooth interpolation and professional easing curves.

commit 05bddae511
Author: John McCardle <mccardle.john@gmail.com>
Date:   Fri Jul 4 06:59:02 2025 -0400

    Update comprehensive documentation for Alpha release (Issue #47)

    - Completely rewrote README.md to reflect current features
    - Updated GitHub Pages documentation site with:
      - Modern landing page highlighting Crypt of Sokoban
      - Comprehensive API reference (2700+ lines) with exhaustive examples
      - Updated getting-started guide with installation and first game tutorial
      - 8 detailed tutorials covering all major game systems
      - Quick reference cheat sheet for common operations
    - Generated documentation screenshots showing UI elements
    - Fixed deprecated API references and added new features
    - Added automation API documentation
    - Included Python 3.12 requirement and platform-specific instructions

    Note: Text rendering in headless mode has limitations for screenshots

commit af6a5e090b
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:43:58 2025 -0400

    Update ROADMAP.md to reflect completion of Issues #2 and #3

    - Marked both issues as completed with the removal of deprecated action system
    - Updated open issue count from ~50 to ~48
    - These were both Alpha blockers, bringing us closer to release

commit 281800cd23
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:43:22 2025 -0400

    Remove deprecated registerPyAction/registerInputAction system (closes #2, closes #3)

    This is our largest net-negative commit yet\! Removed the entire deprecated
    action registration system that provided unnecessary two-step indirection:
    keyboard → action string → Python callback

    Removed components:
    - McRFPy_API::_registerPyAction() and _registerInputAction() methods
    - McRFPy_API::callbacks map for storing Python callables
    - McRFPy_API::doAction() method for executing callbacks
    - ACTIONPY macro from Scene.h for detecting "_py" suffixed actions
    - Scene::registerActionInjected() and unregisterActionInjected() methods
    - tests/api_registerPyAction_issue2_test.py (tested deprecated functionality)

    The game now exclusively uses keypressScene() for keyboard input handling,
    which is simpler and more direct. Also commented out the unused _camFollow
    function that referenced non-existent do_camfollow variable.

commit cc8a7d20e8
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:13:59 2025 -0400

    Clean up temporary test files

commit ff83fd8bb1
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:13:46 2025 -0400

    Update ROADMAP.md to reflect massive progress today

    - Fixed 12+ critical bugs in a single session
    - Implemented 3 missing features (Entity.index, EntityCollection.extend, sprite validation)
    - Updated Phase 1 progress showing 11 of 12 items complete
    - Added detailed summary of today's achievements with issue numbers
    - Emphasized test-driven development approach used throughout

commit dae400031f
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:12:29 2025 -0400

    Remove deprecated player_input and turn-based functions for Issue #3

    Removed the commented-out player_input(), computerTurn(), and playerTurn()
    functions that were part of the old turn-based system. These are no longer
    needed as input is now handled through Scene callbacks.

    Partial fix for #3

commit cb0130b46e
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:09:06 2025 -0400

    Implement sprite index validation for Issue #33

    Added validation to prevent setting sprite indices outside the valid
    range for a texture. The implementation:
    - Adds getSpriteCount() method to PyTexture to expose total sprites
    - Validates sprite_number setter to ensure index is within bounds
    - Provides clear error messages showing valid range
    - Works for both Sprite and Entity objects

    closes #33

commit 1e7f5e9e7e
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:05:47 2025 -0400

    Implement EntityCollection.extend() method for Issue #27

    Added extend() method to EntityCollection that accepts any iterable
    of Entity objects and adds them all to the collection. The method:
    - Accepts lists, tuples, generators, or any iterable
    - Validates all items are Entity objects
    - Sets the grid association for each added entity
    - Properly handles errors and empty iterables

    closes #27

commit 923350137d
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 21:02:14 2025 -0400

    Implement Entity.index() method for Issue #73

    Added index() method to Entity class that returns the entity's
    position in its parent grid's entity collection. This enables
    proper entity removal patterns using entity.index().

commit 6134869371
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 20:41:03 2025 -0400

    Add validation to keypressScene() for non-callable arguments

    Added PyCallable_Check validation to ensure keypressScene() only
    accepts callable objects. Now properly raises TypeError with a
    clear error message when passed non-callable arguments like
    strings, numbers, None, or dicts.

commit 4715356b5e
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 20:31:36 2025 -0400

    Fix Sprite texture setter 'error return without exception set'

    Implemented the missing UISprite::set_texture method to properly:
    - Validate the input is a Texture instance
    - Update the sprite's texture using setTexture()
    - Return appropriate error messages for invalid inputs

    The setter now works correctly and no longer returns -1 without
    setting an exception.

commit 6dd1cec600
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 20:27:32 2025 -0400

    Fix Entity property setters and PyVector implementation

    Fixed the 'new style getargs format' error in Entity property setters by:
    - Implementing PyObject_to_sfVector2f/2i using PyVector::from_arg
    - Adding proper error checking in Entity::set_position
    - Implementing PyVector get_member/set_member for x/y properties
    - Fixing PyVector::from_arg to handle non-tuple arguments correctly

    Now Entity.pos and Entity.sprite_number setters work correctly with
    proper type validation.

commit f82b861bcd
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 19:48:33 2025 -0400

    Fix Issue #74: Add missing Grid.grid_y property

    Added individual grid_x and grid_y getter properties to the Grid class
    to complement the existing grid_size property. This allows direct access
    to grid dimensions and fixes error messages that referenced these
    properties before they existed.

    closes #74

commit 59e6f8d53d
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 19:42:32 2025 -0400

    Fix Issue #78: Middle mouse click no longer sends 'C' keyboard event

    The bug was caused by accessing event.key.code on a mouse event without
    checking the event type first. Since SFML uses a union for events, this
    read garbage data. The middle mouse button value (2) coincidentally matched
    the keyboard 'C' value (2), causing the spurious keyboard event.

    Fixed by adding event type check before accessing key-specific fields.
    Only keyboard events (KeyPressed/KeyReleased) now trigger key callbacks.

    Test added to verify middle clicks no longer generate keyboard events.

    Closes #78

commit 1c71d8d4f7
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 19:36:15 2025 -0400

    Fix Grid to support None/null texture and fix error message bug

    - Allow Grid to be created with None as texture parameter
    - Use default cell dimensions (16x16) when no texture provided
    - Skip sprite rendering when texture is null, but still render colors
    - Fix issue #77: Corrected copy/paste error in Grid.at() error messages
    - Grid now functional for color-only rendering and entity positioning

    Test created to verify Grid works without texture, showing colored cells.

    Closes #77

commit 18cfe93a44
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 19:25:49 2025 -0400

    Fix --exec interactive prompt bug and create comprehensive test suite

    Major fixes:
    - Fixed --exec entering Python REPL instead of game loop
    - Resolved screenshot transparency issue (requires timer callbacks)
    - Added debug output to trace Python initialization

    Test suite created:
    - 13 comprehensive tests covering all Python-exposed methods
    - Tests use timer callback pattern for proper game loop interaction
    - Discovered multiple critical bugs and missing features

    Critical bugs found:
    - Grid class segfaults on instantiation (blocks all Grid functionality)
    - Issue #78 confirmed: Middle mouse click sends 'C' keyboard event
    - Entity property setters have argument parsing errors
    - Sprite texture setter returns improper error
    - keypressScene() segfaults on non-callable arguments

    Documentation updates:
    - Updated CLAUDE.md with testing guidelines and TDD practices
    - Created test reports documenting all findings
    - Updated ROADMAP.md with test results and new priorities

    The Grid segfault is now the highest priority as it blocks all Grid-based functionality.

commit 9ad0b6850d
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 15:55:24 2025 -0400

    Update ROADMAP.md to reflect Python interpreter and automation API progress

    - Mark #32 (Python interpreter behavior) as 90% complete
      - All major Python flags implemented: -h, -V, -c, -m, -i
      - Script execution with proper sys.argv handling works
      - Only stdin (-) support missing

    - Note that new automation API enables:
      - Automated UI testing capabilities
      - Demo recording and playback
      - Accessibility testing support

    - Flag issues #53 and #45 as potentially aided by automation API

commit 7ec4698653
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 14:57:59 2025 -0400

    Update ROADMAP.md to remove closed issues

    - Remove #72 (iterator improvements - closed)
    - Remove #51 (UIEntity derive from UIDrawable - closed)
    - Update issue counts: 64 open issues from original 78
    - Update dependencies and references to reflect closed issues
    - Clarify that core iterators are complete, only grid points remain

commit 68c1a016b0
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 14:27:01 2025 -0400

    Implement --exec flag and PyAutoGUI-compatible automation API

    - Add --exec flag to execute multiple scripts before main program
    - Scripts are executed in order and share Python interpreter state
    - Implement full PyAutoGUI-compatible automation API in McRFPy_Automation
    - Add screenshot, mouse control, keyboard input capabilities
    - Fix Python initialization issues when multiple scripts are loaded
    - Update CommandLineParser to handle --exec with proper sys.argv management
    - Add comprehensive examples and documentation

    This enables automation testing by allowing test scripts to run alongside
    games using the same Python environment. The automation API provides
    event injection into the SFML render loop for UI testing.

    Closes #32 partially (Python interpreter emulation)
    References automation testing requirements

commit 763fa201f0
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 10:43:17 2025 -0400

    Python command emulation

commit a44b8c93e9
Author: John McCardle <mccardle.john@gmail.com>
Date:   Thu Jul 3 09:42:46 2025 -0400

    Prep: Cleanup for interpreter mode
2025-07-05 12:04:20 -04:00
384 changed files with 8102 additions and 1784773 deletions

View File

@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
Test for Entity property setters - fixing "new style getargs format" error
Verifies that Entity position and sprite_number setters work correctly.
"""
def test_entity_setters(timer_name):
"""Test that Entity property setters work correctly"""
import mcrfpy
print("Testing Entity property setters...")
# Create test scene and grid
mcrfpy.createScene("entity_test")
ui = mcrfpy.sceneUI("entity_test")
# Create grid with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
# Create entity
initial_pos = mcrfpy.Vector(2.5, 3.5)
entity = mcrfpy.Entity(initial_pos, texture, 5, grid)
grid.entities.append(entity)
print(f"✓ Created entity at position {entity.pos}")
# Test position setter with Vector
new_pos = mcrfpy.Vector(4.0, 5.0)
try:
entity.pos = new_pos
assert entity.pos.x == 4.0, f"Expected x=4.0, got {entity.pos.x}"
assert entity.pos.y == 5.0, f"Expected y=5.0, got {entity.pos.y}"
print(f"✓ Position setter works with Vector: {entity.pos}")
except Exception as e:
print(f"✗ Position setter failed: {e}")
raise
# Test position setter with tuple (should also work via PyVector::from_arg)
try:
entity.pos = (7.5, 8.5)
assert entity.pos.x == 7.5, f"Expected x=7.5, got {entity.pos.x}"
assert entity.pos.y == 8.5, f"Expected y=8.5, got {entity.pos.y}"
print(f"✓ Position setter works with tuple: {entity.pos}")
except Exception as e:
print(f"✗ Position setter with tuple failed: {e}")
raise
# Test draw_pos setter (collision position)
try:
entity.draw_pos = mcrfpy.Vector(3, 4)
assert entity.draw_pos.x == 3, f"Expected x=3, got {entity.draw_pos.x}"
assert entity.draw_pos.y == 4, f"Expected y=4, got {entity.draw_pos.y}"
print(f"✓ Draw position setter works: {entity.draw_pos}")
except Exception as e:
print(f"✗ Draw position setter failed: {e}")
raise
# Test sprite_number setter
try:
entity.sprite_number = 10
assert entity.sprite_number == 10, f"Expected sprite_number=10, got {entity.sprite_number}"
print(f"✓ Sprite number setter works: {entity.sprite_number}")
except Exception as e:
print(f"✗ Sprite number setter failed: {e}")
raise
# Test invalid position setter (should raise TypeError)
try:
entity.pos = "invalid"
print("✗ Position setter should have raised TypeError for string")
assert False, "Should have raised TypeError"
except TypeError as e:
print(f"✓ Position setter correctly rejects invalid type: {e}")
except Exception as e:
print(f"✗ Unexpected error: {e}")
raise
# Test invalid sprite number (should raise TypeError)
try:
entity.sprite_number = "invalid"
print("✗ Sprite number setter should have raised TypeError for string")
assert False, "Should have raised TypeError"
except TypeError as e:
print(f"✓ Sprite number setter correctly rejects invalid type: {e}")
except Exception as e:
print(f"✗ Unexpected error: {e}")
raise
# Cleanup timer
mcrfpy.delTimer("test_timer")
print("\n✅ Entity property setters test PASSED - All setters work correctly")
# Execute the test after a short delay to ensure window is ready
import mcrfpy
mcrfpy.setTimer("test_timer", test_entity_setters, 100)

View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
Simple test for Entity property setters
"""
def test_entity_setters(timer_name):
"""Test Entity property setters"""
import mcrfpy
import sys
print("Testing Entity property setters...")
# Create test scene and grid
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create grid with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
# Create entity
entity = mcrfpy.Entity((2.5, 3.5), texture, 5, grid)
grid.entities.append(entity)
# Test 1: Initial position
print(f"Initial position: {entity.pos}")
print(f"Initial position x={entity.pos.x}, y={entity.pos.y}")
# Test 2: Set position with Vector
entity.pos = mcrfpy.Vector(4.0, 5.0)
print(f"After Vector setter: pos={entity.pos}, x={entity.pos.x}, y={entity.pos.y}")
# Test 3: Set position with tuple
entity.pos = (7.5, 8.5)
print(f"After tuple setter: pos={entity.pos}, x={entity.pos.x}, y={entity.pos.y}")
# Test 4: sprite_number
print(f"Initial sprite_number: {entity.sprite_number}")
entity.sprite_number = 10
print(f"After setter: sprite_number={entity.sprite_number}")
# Test 5: Invalid types
try:
entity.pos = "invalid"
print("ERROR: Should have raised TypeError")
except TypeError as e:
print(f"✓ Correctly rejected invalid position: {e}")
try:
entity.sprite_number = "invalid"
print("ERROR: Should have raised TypeError")
except TypeError as e:
print(f"✓ Correctly rejected invalid sprite_number: {e}")
print("\n✅ Entity property setters test completed")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_entity_setters, 100)

View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Test for Issue #27: EntityCollection.extend() method
Verifies that EntityCollection can extend with multiple entities at once.
"""
def test_entity_extend(timer_name):
"""Test that EntityCollection.extend() method works correctly"""
import mcrfpy
import sys
print("Issue #27 test: EntityCollection.extend() method")
# Create test scene and grid
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create grid with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
# Add some initial entities
entity1 = mcrfpy.Entity((1, 1), texture, 1, grid)
entity2 = mcrfpy.Entity((2, 2), texture, 2, grid)
grid.entities.append(entity1)
grid.entities.append(entity2)
print(f"✓ Initial entities: {len(grid.entities)}")
# Test 1: Extend with a list of entities
new_entities = [
mcrfpy.Entity((3, 3), texture, 3, grid),
mcrfpy.Entity((4, 4), texture, 4, grid),
mcrfpy.Entity((5, 5), texture, 5, grid)
]
try:
grid.entities.extend(new_entities)
assert len(grid.entities) == 5, f"Expected 5 entities, got {len(grid.entities)}"
print(f"✓ Extended with list: now {len(grid.entities)} entities")
except Exception as e:
print(f"✗ Failed to extend with list: {e}")
raise
# Test 2: Extend with a tuple
more_entities = (
mcrfpy.Entity((6, 6), texture, 6, grid),
mcrfpy.Entity((7, 7), texture, 7, grid)
)
try:
grid.entities.extend(more_entities)
assert len(grid.entities) == 7, f"Expected 7 entities, got {len(grid.entities)}"
print(f"✓ Extended with tuple: now {len(grid.entities)} entities")
except Exception as e:
print(f"✗ Failed to extend with tuple: {e}")
raise
# Test 3: Extend with generator expression
try:
grid.entities.extend(mcrfpy.Entity((8, i), texture, 8+i, grid) for i in range(3))
assert len(grid.entities) == 10, f"Expected 10 entities, got {len(grid.entities)}"
print(f"✓ Extended with generator: now {len(grid.entities)} entities")
except Exception as e:
print(f"✗ Failed to extend with generator: {e}")
raise
# Test 4: Verify all entities have correct grid association
for i, entity in enumerate(grid.entities):
# Just checking that we can iterate and access them
assert entity.sprite_number >= 1, f"Entity {i} has invalid sprite number"
print("✓ All entities accessible and valid")
# Test 5: Invalid input - non-iterable
try:
grid.entities.extend(42)
print("✗ Should have raised TypeError for non-iterable")
except TypeError as e:
print(f"✓ Correctly rejected non-iterable: {e}")
# Test 6: Invalid input - iterable with non-Entity
try:
grid.entities.extend([entity1, "not an entity", entity2])
print("✗ Should have raised TypeError for non-Entity in iterable")
except TypeError as e:
print(f"✓ Correctly rejected non-Entity in iterable: {e}")
# Test 7: Empty iterable (should work)
initial_count = len(grid.entities)
try:
grid.entities.extend([])
assert len(grid.entities) == initial_count, "Empty extend changed count"
print("✓ Empty extend works correctly")
except Exception as e:
print(f"✗ Empty extend failed: {e}")
raise
print(f"\n✅ Issue #27 test PASSED - EntityCollection.extend() works correctly")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_entity_extend, 100)

View File

@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
Test for Issue #33: Sprite index validation
Verifies that Sprite and Entity objects validate sprite indices
against the texture's actual sprite count.
"""
def test_sprite_index_validation(timer_name):
"""Test that sprite index validation works correctly"""
import mcrfpy
import sys
print("Issue #33 test: Sprite index validation")
# Create test scene
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create texture - kenney_ice.png is 11x12 sprites of 16x16 each
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
# Total sprites = 11 * 12 = 132 sprites (indices 0-131)
# Test 1: Create sprite with valid index
try:
sprite = mcrfpy.Sprite(100, 100, texture, 50) # Valid index
ui.append(sprite)
print(f"✓ Created sprite with valid index 50")
except Exception as e:
print(f"✗ Failed to create sprite with valid index: {e}")
raise
# Test 2: Set valid sprite index
try:
sprite.sprite_number = 100 # Still valid
assert sprite.sprite_number == 100
print(f"✓ Set sprite to valid index 100")
except Exception as e:
print(f"✗ Failed to set valid sprite index: {e}")
raise
# Test 3: Set maximum valid index
try:
sprite.sprite_number = 131 # Maximum valid index
assert sprite.sprite_number == 131
print(f"✓ Set sprite to maximum valid index 131")
except Exception as e:
print(f"✗ Failed to set maximum valid index: {e}")
raise
# Test 4: Invalid negative index
try:
sprite.sprite_number = -1
print("✗ Should have raised ValueError for negative index")
except ValueError as e:
print(f"✓ Correctly rejected negative index: {e}")
except Exception as e:
print(f"✗ Wrong exception type for negative index: {e}")
raise
# Test 5: Invalid index too large
try:
sprite.sprite_number = 132 # One past the maximum
print("✗ Should have raised ValueError for index 132")
except ValueError as e:
print(f"✓ Correctly rejected out-of-bounds index: {e}")
except Exception as e:
print(f"✗ Wrong exception type for out-of-bounds index: {e}")
raise
# Test 6: Very large invalid index
try:
sprite.sprite_number = 1000
print("✗ Should have raised ValueError for index 1000")
except ValueError as e:
print(f"✓ Correctly rejected large invalid index: {e}")
# Test 7: Entity sprite_number validation
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
entity = mcrfpy.Entity((5, 5), texture, 50, grid)
grid.entities.append(entity)
try:
entity.sprite_number = 200 # Out of bounds
print("✗ Entity should also validate sprite indices")
except ValueError as e:
print(f"✓ Entity also validates sprite indices: {e}")
except Exception as e:
# Entity might not have the same validation yet
print(f"Note: Entity validation not implemented yet: {e}")
# Test 8: Different texture sizes
# Create a smaller texture to test different bounds
small_texture = mcrfpy.Texture("assets/Sprite-0001.png", 32, 32)
small_sprite = mcrfpy.Sprite(200, 200, small_texture, 0)
# This texture might have fewer sprites, test accordingly
try:
small_sprite.sprite_number = 100 # Might be out of bounds
print("Note: Small texture accepted index 100")
except ValueError as e:
print(f"✓ Small texture has different bounds: {e}")
print(f"\n✅ Issue #33 test PASSED - Sprite index validation works correctly")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_sprite_index_validation, 100)

View File

@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""
Test for Issue #73: Entity.index() method for removal
Verifies that Entity objects can report their index in the grid's entity collection.
"""
def test_entity_index(timer_name):
"""Test that Entity.index() method works correctly"""
import mcrfpy
import sys
print("Issue #73 test: Entity.index() method")
# Create test scene and grid
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create grid with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
# Create multiple entities
entities = []
for i in range(5):
entity = mcrfpy.Entity((i, i), texture, i, grid)
entities.append(entity)
grid.entities.append(entity)
print(f"✓ Created {len(entities)} entities")
# Test 1: Check each entity knows its index
for expected_idx, entity in enumerate(entities):
try:
actual_idx = entity.index()
assert actual_idx == expected_idx, f"Expected index {expected_idx}, got {actual_idx}"
print(f"✓ Entity {expected_idx} correctly reports index {actual_idx}")
except Exception as e:
print(f"✗ Entity {expected_idx} index() failed: {e}")
raise
# Test 2: Remove entity using index
entity_to_remove = entities[2]
remove_idx = entity_to_remove.index()
grid.entities.remove(remove_idx)
print(f"✓ Removed entity at index {remove_idx}")
# Test 3: Verify indices updated after removal
for i, entity in enumerate(entities):
if i == 2:
# This entity was removed, should raise error
try:
idx = entity.index()
print(f"✗ Removed entity still reports index {idx}")
except ValueError as e:
print(f"✓ Removed entity correctly raises error: {e}")
elif i < 2:
# These entities should keep their indices
idx = entity.index()
assert idx == i, f"Entity before removal has wrong index: {idx}"
else:
# These entities should have shifted down by 1
idx = entity.index()
assert idx == i - 1, f"Entity after removal has wrong index: {idx}"
# Test 4: Entity without grid
orphan_entity = mcrfpy.Entity((0, 0), texture, 0, None)
try:
idx = orphan_entity.index()
print(f"✗ Orphan entity should raise error but returned {idx}")
except RuntimeError as e:
print(f"✓ Orphan entity correctly raises error: {e}")
# Test 5: Use index() in practical removal pattern
# Add some new entities
for i in range(3):
entity = mcrfpy.Entity((7+i, 7+i), texture, 10+i, grid)
grid.entities.append(entity)
# Remove entities with sprite_number > 10
removed_count = 0
i = 0
while i < len(grid.entities):
entity = grid.entities[i]
if entity.sprite_number > 10:
grid.entities.remove(entity.index())
removed_count += 1
# Don't increment i, as entities shifted down
else:
i += 1
print(f"✓ Removed {removed_count} entities using index() in loop")
assert len(grid.entities) == 5, f"Expected 5 entities remaining, got {len(grid.entities)}"
print("\n✅ Issue #73 test PASSED - Entity.index() method works correctly")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_entity_index, 100)

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
Simple test for Issue #73: Entity.index() method
"""
def test_entity_index(timer_name):
"""Test that Entity.index() method works correctly"""
import mcrfpy
import sys
print("Testing Entity.index() method...")
# Create test scene and grid
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create grid with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
ui.append(grid)
# Clear any existing entities
while len(grid.entities) > 0:
grid.entities.remove(0)
# Create entities
entity1 = mcrfpy.Entity((1, 1), texture, 1, grid)
entity2 = mcrfpy.Entity((2, 2), texture, 2, grid)
entity3 = mcrfpy.Entity((3, 3), texture, 3, grid)
grid.entities.append(entity1)
grid.entities.append(entity2)
grid.entities.append(entity3)
print(f"Created {len(grid.entities)} entities")
# Test index() method
idx1 = entity1.index()
idx2 = entity2.index()
idx3 = entity3.index()
print(f"Entity 1 index: {idx1}")
print(f"Entity 2 index: {idx2}")
print(f"Entity 3 index: {idx3}")
assert idx1 == 0, f"Entity 1 should be at index 0, got {idx1}"
assert idx2 == 1, f"Entity 2 should be at index 1, got {idx2}"
assert idx3 == 2, f"Entity 3 should be at index 2, got {idx3}"
print("✓ All entities report correct indices")
# Test removal using index
remove_idx = entity2.index()
grid.entities.remove(remove_idx)
print(f"✓ Removed entity at index {remove_idx}")
# Check remaining entities
assert len(grid.entities) == 2
assert entity1.index() == 0
assert entity3.index() == 1 # Should have shifted down
print("✓ Indices updated correctly after removal")
# Test entity not in grid
orphan = mcrfpy.Entity((5, 5), texture, 5, None)
try:
idx = orphan.index()
print(f"✗ Orphan entity should raise error but returned {idx}")
except RuntimeError as e:
print(f"✓ Orphan entity correctly raises error")
print("\n✅ Entity.index() test PASSED")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_entity_index, 100)

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""
Test for Issue #74: Add missing Grid.grid_y property
Verifies that Grid objects expose grid_x and grid_y properties correctly.
"""
def test_grid_xy_properties(timer_name):
"""Test that Grid has grid_x and grid_y properties"""
import mcrfpy
# Test was run
print("Issue #74 test: Grid.grid_x and Grid.grid_y properties")
# Test with texture
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
grid = mcrfpy.Grid(20, 15, texture, (0, 0), (800, 600))
# Test grid_x property
assert hasattr(grid, 'grid_x'), "Grid should have grid_x property"
assert grid.grid_x == 20, f"Expected grid_x=20, got {grid.grid_x}"
print(f"✓ grid.grid_x = {grid.grid_x}")
# Test grid_y property
assert hasattr(grid, 'grid_y'), "Grid should have grid_y property"
assert grid.grid_y == 15, f"Expected grid_y=15, got {grid.grid_y}"
print(f"✓ grid.grid_y = {grid.grid_y}")
# Test grid_size still works
assert hasattr(grid, 'grid_size'), "Grid should still have grid_size property"
assert grid.grid_size == (20, 15), f"Expected grid_size=(20, 15), got {grid.grid_size}"
print(f"✓ grid.grid_size = {grid.grid_size}")
# Test without texture
grid2 = mcrfpy.Grid(30, 25, None, (10, 10), (480, 400))
assert grid2.grid_x == 30, f"Expected grid_x=30, got {grid2.grid_x}"
assert grid2.grid_y == 25, f"Expected grid_y=25, got {grid2.grid_y}"
assert grid2.grid_size == (30, 25), f"Expected grid_size=(30, 25), got {grid2.grid_size}"
print("✓ Grid without texture also has correct grid_x and grid_y")
# Test using in error message context (original issue)
try:
grid.at((-1, 0)) # Should raise error
except ValueError as e:
error_msg = str(e)
assert "Grid.grid_x" in error_msg, f"Error message should reference Grid.grid_x: {error_msg}"
print(f"✓ Error message correctly references Grid.grid_x: {error_msg}")
try:
grid.at((0, -1)) # Should raise error
except ValueError as e:
error_msg = str(e)
assert "Grid.grid_y" in error_msg, f"Error message should reference Grid.grid_y: {error_msg}"
print(f"✓ Error message correctly references Grid.grid_y: {error_msg}")
print("\n✅ Issue #74 test PASSED - Grid.grid_x and Grid.grid_y properties work correctly")
# Execute the test after a short delay to ensure window is ready
import mcrfpy
mcrfpy.setTimer("test_timer", test_grid_xy_properties, 100)

View File

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Test that Issue #78 is fixed - Middle Mouse Click should NOT send 'C' keyboard event"""
import mcrfpy
from mcrfpy import automation
import sys
# Track events
keyboard_events = []
click_events = []
def keyboard_handler(key):
"""Track keyboard events"""
keyboard_events.append(key)
print(f"Keyboard event received: '{key}'")
def click_handler(x, y, button):
"""Track click events"""
click_events.append((x, y, button))
print(f"Click event received: ({x}, {y}, button={button})")
def test_middle_click_fix(runtime):
"""Test that middle click no longer sends 'C' key event"""
print(f"\n=== Testing Issue #78 Fix (runtime: {runtime}) ===")
# Simulate middle click
print("\nSimulating middle click at (200, 200)...")
automation.middleClick(200, 200)
# Also test other clicks for comparison
print("Simulating left click at (100, 100)...")
automation.click(100, 100)
print("Simulating right click at (300, 300)...")
automation.rightClick(300, 300)
# Wait a moment for events to process
mcrfpy.setTimer("check_results", check_results, 500)
def check_results(runtime):
"""Check if the bug is fixed"""
print(f"\n=== Results ===")
print(f"Keyboard events received: {len(keyboard_events)}")
print(f"Click events received: {len(click_events)}")
# Check if 'C' was incorrectly triggered
if 'C' in keyboard_events or 'c' in keyboard_events:
print("\n✗ FAIL - Issue #78 still exists: Middle click triggered 'C' keyboard event!")
print(f"Keyboard events: {keyboard_events}")
else:
print("\n✓ PASS - Issue #78 is FIXED: No spurious 'C' keyboard event from middle click!")
# Take screenshot
filename = f"issue78_fixed_{int(runtime)}.png"
automation.screenshot(filename)
print(f"\nScreenshot saved: {filename}")
# Cleanup and exit
mcrfpy.delTimer("check_results")
sys.exit(0)
# Set up test scene
print("Setting up test scene...")
mcrfpy.createScene("issue78_test")
mcrfpy.setScene("issue78_test")
ui = mcrfpy.sceneUI("issue78_test")
# Register keyboard handler
mcrfpy.keypressScene(keyboard_handler)
# Create a clickable frame
frame = mcrfpy.Frame(50, 50, 400, 400,
fill_color=mcrfpy.Color(100, 150, 200),
outline_color=mcrfpy.Color(255, 255, 255),
outline=3.0)
frame.click = click_handler
ui.append(frame)
# Add label
caption = mcrfpy.Caption(mcrfpy.Vector(100, 100),
text="Issue #78 Test - Middle Click",
fill_color=mcrfpy.Color(255, 255, 255))
caption.size = 24
ui.append(caption)
# Schedule test
print("Scheduling test to run after render loop starts...")
mcrfpy.setTimer("test", test_middle_click_fix, 1000)

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
Test for Sprite texture setter - fixing "error return without exception set"
"""
def test_sprite_texture_setter(timer_name):
"""Test that Sprite texture setter works correctly"""
import mcrfpy
import sys
print("Testing Sprite texture setter...")
# Create test scene
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Create textures
texture1 = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
texture2 = mcrfpy.Texture("assets/kenney_lava.png", 16, 16)
# Create sprite with first texture
sprite = mcrfpy.Sprite(100, 100, texture1, 5)
ui.append(sprite)
# Test getting texture
try:
current_texture = sprite.texture
print(f"✓ Got texture: {current_texture}")
except Exception as e:
print(f"✗ Failed to get texture: {e}")
raise
# Test setting new texture
try:
sprite.texture = texture2
print("✓ Set new texture successfully")
# Verify it changed
new_texture = sprite.texture
if new_texture != texture2:
print(f"✗ Texture didn't change properly")
else:
print("✓ Texture changed correctly")
except Exception as e:
print(f"✗ Failed to set texture: {e}")
raise
# Test invalid texture type
try:
sprite.texture = "invalid"
print("✗ Should have raised TypeError for invalid texture")
except TypeError as e:
print(f"✓ Correctly rejected invalid texture: {e}")
except Exception as e:
print(f"✗ Wrong exception type: {e}")
raise
# Test None texture
try:
sprite.texture = None
print("✗ Should have raised TypeError for None texture")
except TypeError as e:
print(f"✓ Correctly rejected None texture: {e}")
# Test that sprite still renders correctly
print("✓ Sprite still renders with new texture")
print("\n✅ Sprite texture setter test PASSED")
sys.exit(0)
# Execute the test after a short delay
import mcrfpy
mcrfpy.setTimer("test", test_sprite_texture_setter, 100)

12
.gitignore vendored
View File

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

7
.gitmodules vendored
View File

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

View File

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

View File

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

626
CLAUDE.md
View File

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

View File

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

128
README.md
View File

@ -3,27 +3,19 @@
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
* Core roguelike logic from libtcod: field of view, pathfinding
* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera
* Simple GUI element system allows keyboard and mouse input, composition
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship"
![ Image ]()
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items.
## Tenets
- **Python & C++ Hand-in-Hand**: Create your game without ever recompiling. Your Python commands create C++ objects, and animations can occur without calling Python at all.
- **Simple Yet Flexible UI System**: Sprites, Grids, Frames, and Captions with full animation support
- **Entity-Component Architecture**: Implement your game objects with Python integration
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod (demos still under construction)
- **Automation API**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
## Quick Start
**Download**:
- The entire McRogueFace visual framework:
- **Sprite**: an image file or one sprite from a shared sprite sheet
- **Caption**: load a font, display text
- **Frame**: A rectangle; put other things on it to move or manage GUIs as modules
- **Grid**: A 2D array of tiles with zoom + position control
- **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path
- **Animation**: Change any property on any of the above over time
```bash
# Clone and build
git clone <wherever you found this repo>
@ -35,43 +27,6 @@ cd build
./mcrogueface
```
## Building from Source
For most users, pre-built releases are available. If you need to build from source:
### Quick Build (with pre-built dependencies)
Download `build_deps.tar.gz` from the releases page, then:
```bash
git clone <repository-url> McRogueFace
cd McRogueFace
tar -xzf /path/to/build_deps.tar.gz
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
```
### Full Build (compiling all dependencies)
```bash
git clone --recursive <repository-url> McRogueFace
cd McRogueFace
# See BUILD_FROM_SOURCE.md for complete instructions
```
**[BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md)** - Complete build guide including:
- System dependency installation
- Compiling SFML, Python, and libtcod-headless from source
- Creating `build_deps` archives for distribution
- Troubleshooting common build issues
### System Requirements
- **Linux**: Debian/Ubuntu tested; other distros should work
- **Windows**: Supported (see build guide for details)
- **macOS**: Untested
## Example: Creating a Simple Scene
```python
@ -94,82 +49,33 @@ mcrfpy.setScene("intro")
## Documentation
### 📚 Developer Documentation
For comprehensive documentation, tutorials, and API reference, visit:
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
For comprehensive documentation about systems, architecture, and development workflows:
**[Project Wiki](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki)**
Key wiki pages:
- **[Home](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Home)** - Documentation hub with multiple entry points
- **[Grid System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Grid-System)** - Three-layer grid architecture
- **[Python Binding System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Python-Binding-System)** - C++/Python integration
- **[Performance and Profiling](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Performance-and-Profiling)** - Optimization tools
- **[Adding Python Bindings](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Adding-Python-Bindings)** - Step-by-step binding guide
- **[Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap)** - All 46 open issues organized by system
### 📖 Development Guides
In the repository root:
- **[CLAUDE.md](CLAUDE.md)** - Build instructions, testing guidelines, common tasks
- **[ROADMAP.md](ROADMAP.md)** - Strategic vision and development phases
- **[roguelike_tutorial/](roguelike_tutorial/)** - Complete roguelike tutorial implementations
## Build Requirements
## Requirements
- C++17 compiler (GCC 7+ or Clang 5+)
- CMake 3.14+
- Python 3.12+
- SFML 2.6
- SFML 2.5+
- Linux or Windows (macOS untested)
## Project Structure
```
McRogueFace/
├── assets/ # Sprites, fonts, audio
├── build/ # Build output directory: zip + ship
│ ├─ (*)assets/ # (copied location of assets)
│ ├─ (*)scripts/ # (copied location of src/scripts)
│ └─ lib/ # SFML, TCOD libraries, Python + standard library / modules
├── deps/ # Python, SFML, and libtcod imports can be tossed in here to build
│ └─ platform/ # windows, linux subdirectories for OS-specific cpython config
├── docs/ # generated HTML, markdown docs
│ └─ stubs/ # .pyi files for editor integration
├── modules/ # git submodules, to build all of McRogueFace's dependencies from source
├── src/ # C++ engine source
│ └─ scripts/ # Python game scripts (copied during build)
├── scripts/ # Python game scripts
├── assets/ # Sprites, fonts, audio
├── build/ # Build output directory
└── tests/ # Automated test suite
└── tools/ # For the McRogueFace ecosystem: docs generation
```
If you are building McRogueFace to implement game logic or scene configuration in C++, you'll have to compile the project.
If you are writing a game in Python using McRogueFace, you only need to rename and zip/distribute the `build` directory.
## Philosophy
- **C++ every frame, Python every tick**: All rendering data is handled in C++. Structure your UI and program animations in Python, and they are rendered without Python. All game logic can be written in Python.
- **No Compiling Required; Zip And Ship**: Implement your game objects with Python, zip up McRogueFace with your "game.py" to ship
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
- **Hands-Off Testing**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
## Contributing
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.
### Issue Tracking
The project uses [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for task tracking and bug reports. Issues are organized with labels:
- **System labels** (grid, animation, python-binding, etc.) - identify which codebase area
- **Priority labels** (tier1-active, tier2-foundation, tier3-future) - development timeline
- **Type labels** (Major Feature, Minor Feature, Bugfix, etc.) - effort and scope
See the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap) on the wiki for organized view of all open tasks.
The project has a private roadmap and issue list. Reach out via email or social media if you have bugs or feature requests.
## License

View File

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

16
_test.py Normal file
View File

@ -0,0 +1,16 @@
import mcrfpy
# Create a new scene
mcrfpy.createScene("intro")
# Add a text caption
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
caption.size = 48
caption.fill_color = (255, 255, 255)
# Add to scene
mcrfpy.sceneUI("intro").append(caption)
# Switch to the scene
mcrfpy.setScene("intro")

127
automation_example.py Normal file
View File

@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""
McRogueFace Automation API Example
This demonstrates how to use the automation API for testing game UIs.
The API is PyAutoGUI-compatible for easy migration of existing tests.
"""
from mcrfpy import automation
import mcrfpy
import time
def automation_demo():
"""Demonstrate all automation API features"""
print("=== McRogueFace Automation API Demo ===\n")
# 1. Screen Information
print("1. Screen Information:")
screen_size = automation.size()
print(f" Screen size: {screen_size[0]}x{screen_size[1]}")
mouse_pos = automation.position()
print(f" Current mouse position: {mouse_pos}")
on_screen = automation.onScreen(100, 100)
print(f" Is (100, 100) on screen? {on_screen}")
print()
# 2. Mouse Movement
print("2. Mouse Movement:")
print(" Moving to center of screen...")
center_x, center_y = screen_size[0]//2, screen_size[1]//2
automation.moveTo(center_x, center_y, duration=0.5)
print(" Moving relative by (100, 100)...")
automation.moveRel(100, 100, duration=0.5)
print()
# 3. Mouse Clicks
print("3. Mouse Clicks:")
print(" Single click...")
automation.click()
time.sleep(0.2)
print(" Double click...")
automation.doubleClick()
time.sleep(0.2)
print(" Right click...")
automation.rightClick()
time.sleep(0.2)
print(" Triple click...")
automation.tripleClick()
print()
# 4. Keyboard Input
print("4. Keyboard Input:")
print(" Typing message...")
automation.typewrite("Hello from McRogueFace automation!", interval=0.05)
print(" Pressing Enter...")
automation.keyDown("enter")
automation.keyUp("enter")
print(" Hotkey Ctrl+A (select all)...")
automation.hotkey("ctrl", "a")
print()
# 5. Drag Operations
print("5. Drag Operations:")
print(" Dragging from current position to (500, 500)...")
automation.dragTo(500, 500, duration=1.0)
print(" Dragging relative by (-100, -100)...")
automation.dragRel(-100, -100, duration=0.5)
print()
# 6. Scroll Operations
print("6. Scroll Operations:")
print(" Scrolling up 5 clicks...")
automation.scroll(5)
time.sleep(0.5)
print(" Scrolling down 5 clicks...")
automation.scroll(-5)
print()
# 7. Screenshots
print("7. Screenshots:")
print(" Taking screenshot...")
success = automation.screenshot("automation_demo_screenshot.png")
print(f" Screenshot saved: {success}")
print()
print("=== Demo Complete ===")
def create_test_ui():
"""Create a simple UI for testing automation"""
print("Creating test UI...")
# Create a test scene
mcrfpy.createScene("automation_test")
mcrfpy.setScene("automation_test")
# Add some UI elements
ui = mcrfpy.sceneUI("automation_test")
# Add a frame
frame = mcrfpy.Frame(50, 50, 300, 200)
ui.append(frame)
# Add a caption
caption = mcrfpy.Caption(60, 60, "Automation Test UI")
ui.append(caption)
print("Test UI created!")
if __name__ == "__main__":
# Create test UI first
create_test_ui()
# Run automation demo
automation_demo()
print("\nYou can now use the automation API to test your game!")

336
automation_exec_examples.py Normal file
View File

@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""
Examples of automation patterns using the proposed --exec flag
Usage:
./mcrogueface game.py --exec automation_basic.py
./mcrogueface game.py --exec automation_stress.py --exec monitor.py
"""
# ===== automation_basic.py =====
# Basic automation that runs alongside the game
import mcrfpy
from mcrfpy import automation
import time
class GameAutomation:
"""Automated testing that runs periodically"""
def __init__(self):
self.test_count = 0
self.test_results = []
def run_test_suite(self):
"""Called by timer - runs one test per invocation"""
test_name = f"test_{self.test_count}"
try:
if self.test_count == 0:
# Test main menu
self.test_main_menu()
elif self.test_count == 1:
# Test inventory
self.test_inventory()
elif self.test_count == 2:
# Test combat
self.test_combat()
else:
# All tests complete
self.report_results()
return
self.test_results.append((test_name, "PASS"))
except Exception as e:
self.test_results.append((test_name, f"FAIL: {e}"))
self.test_count += 1
def test_main_menu(self):
"""Test main menu interactions"""
automation.screenshot("test_main_menu_before.png")
automation.click(400, 300) # New Game button
time.sleep(0.5)
automation.screenshot("test_main_menu_after.png")
def test_inventory(self):
"""Test inventory system"""
automation.hotkey("i") # Open inventory
time.sleep(0.5)
automation.screenshot("test_inventory_open.png")
# Drag item
automation.moveTo(100, 200)
automation.dragTo(200, 200, duration=0.5)
automation.hotkey("i") # Close inventory
def test_combat(self):
"""Test combat system"""
# Move character
automation.keyDown("w")
time.sleep(0.5)
automation.keyUp("w")
# Attack
automation.click(500, 400)
automation.screenshot("test_combat.png")
def report_results(self):
"""Generate test report"""
print("\n=== Automation Test Results ===")
for test, result in self.test_results:
print(f"{test}: {result}")
print(f"Total: {len(self.test_results)} tests")
# Stop the timer
mcrfpy.delTimer("automation_suite")
# Create automation instance and register timer
auto = GameAutomation()
mcrfpy.setTimer("automation_suite", auto.run_test_suite, 2000) # Run every 2 seconds
print("Game automation started - tests will run every 2 seconds")
# ===== automation_stress.py =====
# Stress testing with random inputs
import mcrfpy
from mcrfpy import automation
import random
class StressTester:
"""Randomly interact with the game to find edge cases"""
def __init__(self):
self.action_count = 0
self.errors = []
def random_action(self):
"""Perform a random UI action"""
try:
action = random.choice([
self.random_click,
self.random_key,
self.random_drag,
self.random_hotkey
])
action()
self.action_count += 1
# Periodic screenshot
if self.action_count % 50 == 0:
automation.screenshot(f"stress_test_{self.action_count}.png")
print(f"Stress test: {self.action_count} actions performed")
except Exception as e:
self.errors.append((self.action_count, str(e)))
def random_click(self):
x = random.randint(0, 1024)
y = random.randint(0, 768)
button = random.choice(["left", "right"])
automation.click(x, y, button=button)
def random_key(self):
key = random.choice([
"a", "b", "c", "d", "w", "s",
"space", "enter", "escape",
"1", "2", "3", "4", "5"
])
automation.keyDown(key)
automation.keyUp(key)
def random_drag(self):
x1 = random.randint(0, 1024)
y1 = random.randint(0, 768)
x2 = random.randint(0, 1024)
y2 = random.randint(0, 768)
automation.moveTo(x1, y1)
automation.dragTo(x2, y2, duration=0.2)
def random_hotkey(self):
modifier = random.choice(["ctrl", "alt", "shift"])
key = random.choice(["a", "s", "d", "f"])
automation.hotkey(modifier, key)
# Create stress tester and run frequently
stress = StressTester()
mcrfpy.setTimer("stress_test", stress.random_action, 100) # Every 100ms
print("Stress testing started - random actions every 100ms")
# ===== monitor.py =====
# Performance and state monitoring
import mcrfpy
from mcrfpy import automation
import json
import time
class PerformanceMonitor:
"""Monitor game performance and state"""
def __init__(self):
self.samples = []
self.start_time = time.time()
def collect_sample(self):
"""Collect performance data"""
sample = {
"timestamp": time.time() - self.start_time,
"fps": mcrfpy.getFPS() if hasattr(mcrfpy, 'getFPS') else 60,
"scene": mcrfpy.currentScene(),
"memory": self.estimate_memory_usage()
}
self.samples.append(sample)
# Log every 10 samples
if len(self.samples) % 10 == 0:
avg_fps = sum(s["fps"] for s in self.samples[-10:]) / 10
print(f"Average FPS (last 10 samples): {avg_fps:.1f}")
# Save data every 100 samples
if len(self.samples) % 100 == 0:
self.save_report()
def estimate_memory_usage(self):
"""Estimate memory usage based on scene complexity"""
# This is a placeholder - real implementation would use psutil
ui_count = len(mcrfpy.sceneUI(mcrfpy.currentScene()))
return ui_count * 1000 # Rough estimate in KB
def save_report(self):
"""Save performance report"""
with open("performance_report.json", "w") as f:
json.dump({
"samples": self.samples,
"summary": {
"total_samples": len(self.samples),
"duration": time.time() - self.start_time,
"avg_fps": sum(s["fps"] for s in self.samples) / len(self.samples)
}
}, f, indent=2)
print(f"Performance report saved ({len(self.samples)} samples)")
# Create monitor and start collecting
monitor = PerformanceMonitor()
mcrfpy.setTimer("performance_monitor", monitor.collect_sample, 1000) # Every second
print("Performance monitoring started - sampling every second")
# ===== automation_replay.py =====
# Record and replay user actions
import mcrfpy
from mcrfpy import automation
import json
import time
class ActionRecorder:
"""Record user actions for replay"""
def __init__(self):
self.recording = False
self.actions = []
self.start_time = None
def start_recording(self):
"""Start recording user actions"""
self.recording = True
self.actions = []
self.start_time = time.time()
print("Recording started - perform actions to record")
# Register callbacks for all input types
mcrfpy.registerPyAction("record_click", self.record_click)
mcrfpy.registerPyAction("record_key", self.record_key)
# Map all mouse buttons
for button in range(3):
mcrfpy.registerInputAction(8192 + button, "record_click")
# Map common keys
for key in range(256):
mcrfpy.registerInputAction(4096 + key, "record_key")
def record_click(self, action_type):
"""Record mouse click"""
if not self.recording or action_type != "start":
return
pos = automation.position()
self.actions.append({
"type": "click",
"time": time.time() - self.start_time,
"x": pos[0],
"y": pos[1]
})
def record_key(self, action_type):
"""Record key press"""
if not self.recording or action_type != "start":
return
# This is simplified - real implementation would decode the key
self.actions.append({
"type": "key",
"time": time.time() - self.start_time,
"key": "unknown"
})
def stop_recording(self):
"""Stop recording and save"""
self.recording = False
with open("recorded_actions.json", "w") as f:
json.dump(self.actions, f, indent=2)
print(f"Recording stopped - {len(self.actions)} actions saved")
def replay_actions(self):
"""Replay recorded actions"""
print("Replaying recorded actions...")
with open("recorded_actions.json", "r") as f:
actions = json.load(f)
start_time = time.time()
action_index = 0
def replay_next():
nonlocal action_index
if action_index >= len(actions):
print("Replay complete")
mcrfpy.delTimer("replay")
return
action = actions[action_index]
current_time = time.time() - start_time
# Wait until it's time for this action
if current_time >= action["time"]:
if action["type"] == "click":
automation.click(action["x"], action["y"])
elif action["type"] == "key":
automation.keyDown(action["key"])
automation.keyUp(action["key"])
action_index += 1
mcrfpy.setTimer("replay", replay_next, 10) # Check every 10ms
# Example usage - would be controlled by UI
recorder = ActionRecorder()
# To start recording:
# recorder.start_recording()
# To stop and save:
# recorder.stop_recording()
# To replay:
# recorder.replay_actions()
print("Action recorder ready - call recorder.start_recording() to begin")

View File

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

View File

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

33
clean.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
# Clean script for McRogueFace - removes build artifacts
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}Cleaning McRogueFace build artifacts...${NC}"
# Remove build directory
if [ -d "build" ]; then
echo "Removing build directory..."
rm -rf build
fi
# Remove CMake artifacts from project root
echo "Removing CMake artifacts from project root..."
rm -f CMakeCache.txt
rm -f cmake_install.cmake
rm -f Makefile
rm -rf CMakeFiles
# Remove compiled executable from project root
rm -f mcrogueface
# Remove any test artifacts
rm -f test_script.py
rm -rf test_venv
rm -f python3 # symlink
echo -e "${GREEN}Clean complete!${NC}"

157
css_colors.txt Normal file
View File

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

BIN
debug_immediate.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
debug_multi_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
debug_multi_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
debug_multi_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

63
example_automation.py Normal file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""
Example automation script using --exec flag
Usage: ./mcrogueface game.py --exec example_automation.py
"""
import mcrfpy
from mcrfpy import automation
class GameAutomation:
def __init__(self):
self.frame_count = 0
self.test_phase = 0
print("Automation: Initialized")
def periodic_test(self):
"""Called every second to perform automation tasks"""
self.frame_count = mcrfpy.getFrame()
print(f"Automation: Running test at frame {self.frame_count}")
# Take periodic screenshots
if self.test_phase % 5 == 0:
filename = f"automation_screenshot_{self.test_phase}.png"
automation.screenshot(filename)
print(f"Automation: Saved {filename}")
# Simulate user input based on current scene
scene = mcrfpy.currentScene()
print(f"Automation: Current scene is '{scene}'")
if scene == "main_menu" and self.test_phase < 5:
# Click start button
automation.click(512, 400)
print("Automation: Clicked start button")
elif scene == "game":
# Perform game actions
if self.test_phase % 3 == 0:
automation.hotkey("i") # Toggle inventory
print("Automation: Toggled inventory")
else:
# Random movement
import random
key = random.choice(["w", "a", "s", "d"])
automation.keyDown(key)
automation.keyUp(key)
print(f"Automation: Pressed '{key}' key")
self.test_phase += 1
# Stop after 20 tests
if self.test_phase >= 20:
print("Automation: Test suite complete")
mcrfpy.delTimer("automation_test")
# Could also call mcrfpy.quit() to exit the game
# Create automation instance
automation_instance = GameAutomation()
# Register periodic timer
mcrfpy.setTimer("automation_test", automation_instance.periodic_test, 1000)
print("Automation: Script loaded - tests will run every second")
print("Automation: The game and automation share the same Python environment")

53
example_config.py Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""
Example configuration script that sets up shared state for other scripts
Usage: ./mcrogueface --exec example_config.py --exec example_automation.py game.py
"""
import mcrfpy
# Create a shared configuration namespace
class AutomationConfig:
# Test settings
test_enabled = True
screenshot_interval = 5 # Take screenshot every N tests
max_test_count = 50
test_delay_ms = 1000
# Monitoring settings
monitor_enabled = True
monitor_interval_ms = 500
report_delay_seconds = 30
# Game-specific settings
start_button_pos = (512, 400)
inventory_key = "i"
movement_keys = ["w", "a", "s", "d"]
# Shared state
test_results = []
performance_data = []
@classmethod
def log_result(cls, test_name, success, details=""):
"""Log a test result"""
cls.test_results.append({
"test": test_name,
"success": success,
"details": details,
"frame": mcrfpy.getFrame()
})
@classmethod
def get_summary(cls):
"""Get test summary"""
total = len(cls.test_results)
passed = sum(1 for r in cls.test_results if r["success"])
return f"Tests: {passed}/{total} passed"
# Attach config to mcrfpy module so other scripts can access it
mcrfpy.automation_config = AutomationConfig
print("Config: Automation configuration loaded")
print(f"Config: Test delay = {AutomationConfig.test_delay_ms}ms")
print(f"Config: Max tests = {AutomationConfig.max_test_count}")
print("Config: Other scripts can access config via mcrfpy.automation_config")

69
example_monitoring.py Normal file
View File

@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
Example monitoring script that works alongside automation
Usage: ./mcrogueface game.py --exec example_automation.py --exec example_monitoring.py
"""
import mcrfpy
import time
class PerformanceMonitor:
def __init__(self):
self.start_time = time.time()
self.frame_samples = []
self.scene_changes = []
self.last_scene = None
print("Monitor: Performance monitoring initialized")
def collect_metrics(self):
"""Collect performance and state metrics"""
current_frame = mcrfpy.getFrame()
current_time = time.time() - self.start_time
current_scene = mcrfpy.currentScene()
# Track frame rate
if len(self.frame_samples) > 0:
last_frame, last_time = self.frame_samples[-1]
fps = (current_frame - last_frame) / (current_time - last_time)
print(f"Monitor: FPS = {fps:.1f}")
self.frame_samples.append((current_frame, current_time))
# Track scene changes
if current_scene != self.last_scene:
print(f"Monitor: Scene changed from '{self.last_scene}' to '{current_scene}'")
self.scene_changes.append((current_time, self.last_scene, current_scene))
self.last_scene = current_scene
# Keep only last 100 samples
if len(self.frame_samples) > 100:
self.frame_samples = self.frame_samples[-100:]
def generate_report(self):
"""Generate a summary report"""
if len(self.frame_samples) < 2:
return
total_frames = self.frame_samples[-1][0] - self.frame_samples[0][0]
total_time = self.frame_samples[-1][1] - self.frame_samples[0][1]
avg_fps = total_frames / total_time
print("\n=== Performance Report ===")
print(f"Monitor: Total time: {total_time:.1f} seconds")
print(f"Monitor: Total frames: {total_frames}")
print(f"Monitor: Average FPS: {avg_fps:.1f}")
print(f"Monitor: Scene changes: {len(self.scene_changes)}")
# Stop monitoring
mcrfpy.delTimer("performance_monitor")
# Create monitor instance
monitor = PerformanceMonitor()
# Register monitoring timer (runs every 500ms)
mcrfpy.setTimer("performance_monitor", monitor.collect_metrics, 500)
# Register report generation (runs after 30 seconds)
mcrfpy.setTimer("performance_report", monitor.generate_report, 30000)
print("Monitor: Script loaded - collecting metrics every 500ms")
print("Monitor: Will generate report after 30 seconds")

View File

@ -0,0 +1,189 @@
// Example implementation of --exec flag for McRogueFace
// This shows the minimal changes needed to support multiple script execution
// === In McRogueFaceConfig.h ===
struct McRogueFaceConfig {
// ... existing fields ...
// Scripts to execute after main script (McRogueFace style)
std::vector<std::filesystem::path> exec_scripts;
};
// === In CommandLineParser.cpp ===
CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) {
// ... existing parsing code ...
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
// ... existing flag handling ...
else if (arg == "--exec") {
// Add script to exec list
if (i + 1 < argc) {
config.exec_scripts.push_back(argv[++i]);
} else {
std::cerr << "Error: --exec requires a script path\n";
return {true, 1};
}
}
}
}
// === In GameEngine.cpp ===
GameEngine::GameEngine(const McRogueFaceConfig& cfg) : config(cfg) {
// ... existing initialization ...
// Only load game.py if no custom script/command/module is specified
bool should_load_game = config.script_path.empty() &&
config.python_command.empty() &&
config.python_module.empty() &&
!config.interactive_mode &&
!config.python_mode &&
config.exec_scripts.empty(); // Add this check
if (should_load_game) {
if (!Py_IsInitialized()) {
McRFPy_API::api_init();
}
McRFPy_API::executePyString("import mcrfpy");
McRFPy_API::executeScript("scripts/game.py");
}
// Execute any --exec scripts
for (const auto& exec_script : config.exec_scripts) {
std::cout << "Executing script: " << exec_script << std::endl;
McRFPy_API::executeScript(exec_script.string());
}
}
// === Usage Examples ===
// Example 1: Run game with automation
// ./mcrogueface game.py --exec automation.py
// Example 2: Run game with multiple automation scripts
// ./mcrogueface game.py --exec test_suite.py --exec monitor.py --exec logger.py
// Example 3: Run only automation (no game)
// ./mcrogueface --exec standalone_test.py
// Example 4: Headless automation
// ./mcrogueface --headless game.py --exec automation.py
// === Python Script Example (automation.py) ===
/*
import mcrfpy
from mcrfpy import automation
def periodic_test():
"""Run automated tests every 5 seconds"""
# Take screenshot
automation.screenshot(f"test_{mcrfpy.getFrame()}.png")
# Check game state
scene = mcrfpy.currentScene()
if scene == "main_menu":
# Click start button
automation.click(400, 300)
elif scene == "game":
# Perform game tests
automation.hotkey("i") # Open inventory
print(f"Test completed at frame {mcrfpy.getFrame()}")
# Register timer for periodic testing
mcrfpy.setTimer("automation_test", periodic_test, 5000)
print("Automation script loaded - tests will run every 5 seconds")
# Script returns here - giving control back to C++
*/
// === Advanced Example: Event-Driven Automation ===
/*
# automation_advanced.py
import mcrfpy
from mcrfpy import automation
import json
class AutomationFramework:
def __init__(self):
self.test_queue = []
self.results = []
self.load_test_suite()
def load_test_suite(self):
"""Load test definitions from JSON"""
with open("test_suite.json") as f:
self.test_queue = json.load(f)["tests"]
def run_next_test(self):
"""Execute next test in queue"""
if not self.test_queue:
self.finish_testing()
return
test = self.test_queue.pop(0)
try:
if test["type"] == "click":
automation.click(test["x"], test["y"])
elif test["type"] == "key":
automation.keyDown(test["key"])
automation.keyUp(test["key"])
elif test["type"] == "screenshot":
automation.screenshot(test["filename"])
elif test["type"] == "wait":
# Re-queue this test for later
self.test_queue.insert(0, test)
return
self.results.append({"test": test, "status": "pass"})
except Exception as e:
self.results.append({"test": test, "status": "fail", "error": str(e)})
def finish_testing(self):
"""Save test results and cleanup"""
with open("test_results.json", "w") as f:
json.dump(self.results, f, indent=2)
print(f"Testing complete: {len(self.results)} tests executed")
mcrfpy.delTimer("automation_framework")
# Create and start automation
framework = AutomationFramework()
mcrfpy.setTimer("automation_framework", framework.run_next_test, 100)
*/
// === Thread Safety Considerations ===
// The --exec approach requires NO thread safety changes because:
// 1. All scripts run in the same Python interpreter
// 2. Scripts execute sequentially during initialization
// 3. After initialization, only callbacks run (timer/input based)
// 4. C++ maintains control of the render loop
// This is the "honor system" - scripts must:
// - Set up their callbacks/timers
// - Return control to C++
// - Not block or run infinite loops
// - Use timers for periodic tasks
// === Future Extensions ===
// 1. Script communication via shared Python modules
// game.py:
// import mcrfpy
// mcrfpy.game_state = {"level": 1, "score": 0}
//
// automation.py:
// import mcrfpy
// if mcrfpy.game_state["level"] == 1:
// # Test level 1 specific features
// 2. Priority-based script execution
// ./mcrogueface game.py --exec-priority high:critical.py --exec-priority low:logging.py
// 3. Conditional execution
// ./mcrogueface game.py --exec-if-scene menu:menu_test.py --exec-if-scene game:game_test.py

Binary file not shown.

View File

@ -8,7 +8,7 @@ from time import time
print("Fetching issues...", end='')
start = time()
from gitea import Gitea, Repository, Issue
g = Gitea("https://gamedev.ffwf.net/gitea", token_text="febad52bd50f87fb17691c5e972597d6fff73452")
g = Gitea("https://gamedev.ffwf.net/gitea", token_text="3b450f66e21d62c22bb9fa1c8b975049a5d0c38d")
repo = Repository.request(g, "john", "McRogueFace")
issues = repo.get_issues()
dur = time() - start
@ -99,4 +99,4 @@ 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()
plt.show()

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
issue78_fixed_1658.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

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

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

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

1
modules/libtcod Submodule

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,10 +1,6 @@
#include "Animation.h"
#include "UIDrawable.h"
#include "UIEntity.h"
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "GameEngine.h"
#include "PythonObjectCache.h"
#include <cmath>
#include <algorithm>
#include <unordered_map>
@ -13,105 +9,75 @@
#define M_PI 3.14159265358979323846
#endif
// Forward declaration of PyAnimation type
namespace mcrfpydef {
extern PyTypeObject PyAnimationType;
}
// Animation implementation
Animation::Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc,
bool delta,
PyObject* callback)
bool delta)
: targetProperty(targetProperty)
, targetValue(targetValue)
, duration(duration)
, easingFunc(easingFunc)
, delta(delta)
, pythonCallback(callback)
{
// Increase reference count for Python callback
if (pythonCallback) {
Py_INCREF(pythonCallback);
}
}
Animation::~Animation() {
// Decrease reference count for Python callback if we still own it
PyObject* callback = pythonCallback;
if (callback) {
pythonCallback = nullptr;
PyGILState_STATE gstate = PyGILState_Ensure();
Py_DECREF(callback);
PyGILState_Release(gstate);
}
// Clean up cache entry
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
}
void Animation::start(std::shared_ptr<UIDrawable> target) {
if (!target) return;
targetWeak = target;
void Animation::start(UIDrawable* target) {
currentTarget = target;
elapsed = 0.0f;
callbackTriggered = false; // Reset callback state
// Capture start value from target
std::visit([this, &target](const auto& targetVal) {
// Capture startValue from target based on targetProperty
if (!currentTarget) return;
// Try to get the current value based on the expected type
std::visit([this](const auto& targetVal) {
using T = std::decay_t<decltype(targetVal)>;
if constexpr (std::is_same_v<T, float>) {
float value;
if (target->getProperty(targetProperty, value)) {
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, int>) {
int value;
if (target->getProperty(targetProperty, value)) {
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// For sprite animation, get current sprite index
int value;
if (target->getProperty(targetProperty, value)) {
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Color>) {
sf::Color value;
if (target->getProperty(targetProperty, value)) {
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
sf::Vector2f value;
if (target->getProperty(targetProperty, value)) {
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, std::string>) {
std::string value;
if (target->getProperty(targetProperty, value)) {
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
}, targetValue);
}
void Animation::startEntity(std::shared_ptr<UIEntity> target) {
if (!target) return;
entityTargetWeak = target;
void Animation::startEntity(UIEntity* target) {
currentEntityTarget = target;
currentTarget = nullptr; // Clear drawable target
elapsed = 0.0f;
callbackTriggered = false; // Reset callback state
// Capture the starting value from the entity
std::visit([this, target](const auto& val) {
@ -133,49 +99,8 @@ void Animation::startEntity(std::shared_ptr<UIEntity> target) {
}, targetValue);
}
bool Animation::hasValidTarget() const {
return !targetWeak.expired() || !entityTargetWeak.expired();
}
void Animation::clearCallback() {
// Safely clear the callback when PyAnimation is being destroyed
PyObject* callback = pythonCallback;
if (callback) {
pythonCallback = nullptr;
callbackTriggered = true; // Prevent future triggering
PyGILState_STATE gstate = PyGILState_Ensure();
Py_DECREF(callback);
PyGILState_Release(gstate);
}
}
void Animation::complete() {
// Jump to end of animation
elapsed = duration;
// Apply final value
if (auto target = targetWeak.lock()) {
AnimationValue finalValue = interpolate(1.0f);
applyValue(target.get(), finalValue);
}
else if (auto entity = entityTargetWeak.lock()) {
AnimationValue finalValue = interpolate(1.0f);
applyValue(entity.get(), finalValue);
}
}
bool Animation::update(float deltaTime) {
// Try to lock weak_ptr to get shared_ptr
std::shared_ptr<UIDrawable> target = targetWeak.lock();
std::shared_ptr<UIEntity> entity = entityTargetWeak.lock();
// If both are null, target was destroyed
if (!target && !entity) {
return false; // Remove this animation
}
if (isComplete()) {
if ((!currentTarget && !currentEntityTarget) || isComplete()) {
return false;
}
@ -189,18 +114,39 @@ bool Animation::update(float deltaTime) {
// Get interpolated value
AnimationValue currentValue = interpolate(easedT);
// Apply to whichever target is valid
if (target) {
applyValue(target.get(), currentValue);
} else if (entity) {
applyValue(entity.get(), currentValue);
}
// Trigger callback when animation completes
// Check pythonCallback again in case it was cleared during update
if (isComplete() && !callbackTriggered && pythonCallback) {
triggerCallback();
}
// Apply currentValue to target (either drawable or entity)
std::visit([this](const auto& value) {
using T = std::decay_t<decltype(value)>;
if (currentTarget) {
// Handle UIDrawable targets
if constexpr (std::is_same_v<T, float>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, int>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, std::string>) {
currentTarget->setProperty(targetProperty, value);
}
}
else if (currentEntityTarget) {
// Handle UIEntity targets
if constexpr (std::is_same_v<T, float>) {
currentEntityTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, int>) {
currentEntityTarget->setProperty(targetProperty, value);
}
// Entities don't support other types yet
}
}, currentValue);
return !isComplete();
}
@ -308,82 +254,6 @@ AnimationValue Animation::interpolate(float t) const {
}, targetValue);
}
void Animation::applyValue(UIDrawable* target, const AnimationValue& value) {
if (!target) return;
std::visit([this, target](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, int>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, std::string>) {
target->setProperty(targetProperty, val);
}
}, value);
}
void Animation::applyValue(UIEntity* entity, const AnimationValue& value) {
if (!entity) return;
std::visit([this, entity](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
entity->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, int>) {
entity->setProperty(targetProperty, val);
}
// Entities don't support other types yet
}, value);
}
void Animation::triggerCallback() {
if (!pythonCallback) return;
// Ensure we only trigger once
if (callbackTriggered) return;
callbackTriggered = true;
PyGILState_STATE gstate = PyGILState_Ensure();
// TODO: In future, create PyAnimation wrapper for this animation
// For now, pass None for both parameters
PyObject* args = PyTuple_New(2);
Py_INCREF(Py_None);
Py_INCREF(Py_None);
PyTuple_SetItem(args, 0, Py_None); // animation parameter
PyTuple_SetItem(args, 1, Py_None); // target parameter
PyObject* result = PyObject_CallObject(pythonCallback, args);
Py_DECREF(args);
if (!result) {
std::cerr << "Animation callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
// Check if we should exit on exception
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
McRFPy_API::signalPythonException();
}
} else {
Py_DECREF(result);
}
PyGILState_Release(gstate);
}
// Easing functions implementation
namespace EasingFunctions {
@ -632,50 +502,26 @@ AnimationManager& AnimationManager::getInstance() {
}
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
if (animation && animation->hasValidTarget()) {
if (isUpdating) {
// Defer adding during update to avoid iterator invalidation
pendingAnimations.push_back(animation);
} else {
activeAnimations.push_back(animation);
}
}
activeAnimations.push_back(animation);
}
void AnimationManager::update(float deltaTime) {
// Set flag to defer new animations
isUpdating = true;
// Remove completed or invalid animations
for (auto& anim : activeAnimations) {
anim->update(deltaTime);
}
cleanup();
}
void AnimationManager::cleanup() {
activeAnimations.erase(
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
[deltaTime](std::shared_ptr<Animation>& anim) {
return !anim || !anim->update(deltaTime);
[](const std::shared_ptr<Animation>& anim) {
return anim->isComplete();
}),
activeAnimations.end()
);
// Clear update flag
isUpdating = false;
// Add any animations that were created during update
if (!pendingAnimations.empty()) {
activeAnimations.insert(activeAnimations.end(),
pendingAnimations.begin(),
pendingAnimations.end());
pendingAnimations.clear();
}
}
void AnimationManager::clear(bool completeAnimations) {
if (completeAnimations) {
// Complete all animations before clearing
for (auto& anim : activeAnimations) {
if (anim) {
anim->complete();
}
}
}
void AnimationManager::clear() {
activeAnimations.clear();
}

View File

@ -6,7 +6,6 @@
#include <variant>
#include <vector>
#include <SFML/Graphics.hpp>
#include "Python.h"
// Forward declarations
class UIDrawable;
@ -37,20 +36,13 @@ public:
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc = EasingFunctions::linear,
bool delta = false,
PyObject* callback = nullptr);
// Destructor - cleanup Python callback reference
~Animation();
bool delta = false);
// Apply this animation to a drawable
void start(std::shared_ptr<UIDrawable> target);
void start(UIDrawable* target);
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
void startEntity(std::shared_ptr<UIEntity> target);
// Complete the animation immediately (jump to final value)
void complete();
void startEntity(UIEntity* target);
// Update animation (called each frame)
// Returns true if animation is still running, false if complete
@ -59,12 +51,6 @@ public:
// Get current interpolated value
AnimationValue getCurrentValue() const;
// Check if animation has valid target
bool hasValidTarget() const;
// Clear the callback (called when PyAnimation is deallocated)
void clearCallback();
// Animation properties
std::string getTargetProperty() const { return targetProperty; }
float getDuration() const { return duration; }
@ -81,27 +67,11 @@ private:
EasingFunction easingFunc; // Easing function to use
bool delta; // If true, targetValue is relative to start
// RAII: Use weak_ptr for safe target tracking
std::weak_ptr<UIDrawable> targetWeak;
std::weak_ptr<UIEntity> entityTargetWeak;
// Callback support
PyObject* pythonCallback = nullptr; // Python callback function (we own a reference)
bool callbackTriggered = false; // Ensure callback only fires once
PyObject* pyAnimationWrapper = nullptr; // Weak reference to PyAnimation if created from Python
// Python object cache support
uint64_t serial_number = 0;
UIDrawable* currentTarget = nullptr; // Current target being animated
UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable)
// Helper to interpolate between values
AnimationValue interpolate(float t) const;
// Helper to apply value to target
void applyValue(UIDrawable* target, const AnimationValue& value);
void applyValue(UIEntity* entity, const AnimationValue& value);
// Trigger callback when animation completes
void triggerCallback();
};
// Easing functions library
@ -164,12 +134,13 @@ public:
// Update all animations
void update(float deltaTime);
// Clear all animations (optionally completing them first)
void clear(bool completeAnimations = false);
// Remove completed animations
void cleanup();
// Clear all animations
void clear();
private:
AnimationManager() = default;
std::vector<std::shared_ptr<Animation>> activeAnimations;
std::vector<std::shared_ptr<Animation>> pendingAnimations; // Animations to add after update
bool isUpdating = false; // Flag to track if we're in update loop
};

View File

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

View File

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

View File

@ -121,12 +121,6 @@ CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& confi
current_arg++;
continue;
}
if (arg == "--continue-after-exceptions") {
config.exit_on_exception = false;
current_arg++;
continue;
}
// If no flags matched, treat as positional argument (script name)
if (arg[0] != '-') {
@ -166,8 +160,6 @@ void CommandLineParser::print_help() {
<< " --audio-off : disable audio\n"
<< " --audio-on : enable audio (even in headless mode)\n"
<< " --screenshot [path] : take a screenshot in headless mode\n"
<< " --continue-after-exceptions : don't exit on Python callback exceptions\n"
<< " (default: exit on first exception)\n"
<< "\n"
<< "Arguments:\n"
<< " file : program read from script file\n"
@ -176,5 +168,5 @@ void CommandLineParser::print_help() {
}
void CommandLineParser::print_version() {
std::cout << "Python 3.14.0 (McRogueFace embedded)\n";
std::cout << "Python 3.12.0 (McRogueFace embedded)\n";
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@
#include "Common.h"
#include "Python.h"
#include <list>
#include <atomic>
#include "PyFont.h"
#include "PyTexture.h"
@ -29,17 +28,17 @@ public:
//static void setSpriteTexture(int);
inline static GameEngine* game;
static void api_init();
static void api_init(const McRogueFaceConfig& config);
static PyStatus init_python_with_config(const McRogueFaceConfig& config);
static void api_init(const McRogueFaceConfig& config, int argc, char** argv);
static PyStatus init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv);
static void api_shutdown();
// Python API functionality - use mcrfpy.* in scripts
//static PyObject* _drawSprite(PyObject*, PyObject*);
static void REPL_device(FILE * fp, const char *filename);
static void REPL();
static std::vector<sf::SoundBuffer>* soundbuffers;
static sf::Music* music;
static sf::Sound* sfx;
static std::vector<sf::SoundBuffer> soundbuffers;
static sf::Music music;
static sf::Sound sfx;
static PyObject* _createSoundBuffer(PyObject*, PyObject*);
@ -74,30 +73,4 @@ public:
// Helper to mark scenes as needing z_index resort
static void markSceneNeedsSort();
// Name-based finding methods
static PyObject* _find(PyObject*, PyObject*);
static PyObject* _findAll(PyObject*, PyObject*);
// Profiling/metrics
static PyObject* _getMetrics(PyObject*, PyObject*);
// Benchmark logging (#104)
static PyObject* _startBenchmark(PyObject*, PyObject*);
static PyObject* _endBenchmark(PyObject*, PyObject*);
static PyObject* _logBenchmark(PyObject*, PyObject*);
// Developer console
static PyObject* _setDevConsole(PyObject*, PyObject*);
// Scene lifecycle management for Python Scene objects
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
static void updatePythonScenes(float dt);
static void triggerResize(int width, int height);
// Exception handling - signal game loop to exit on unhandled Python exceptions
static std::atomic<bool> exception_occurred;
static std::atomic<int> exit_code;
static void signalPythonException(); // Called by exception handlers
static bool shouldExit(); // Checked by game loop
};

View File

@ -6,14 +6,6 @@
#include <sstream>
#include <unordered_map>
// #111 - Static member for simulated mouse position in headless mode
sf::Vector2i McRFPy_Automation::simulated_mouse_pos(0, 0);
// #111 - Get simulated mouse position for headless mode
sf::Vector2i McRFPy_Automation::getSimulatedMousePosition() {
return simulated_mouse_pos;
}
// Helper function to get game engine
GameEngine* McRFPy_Automation::getGameEngine() {
return McRFPy_API::game;
@ -114,17 +106,10 @@ sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) {
void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button) {
auto engine = getGameEngine();
if (!engine) return;
// #111 - Track simulated mouse position for headless mode
if (type == sf::Event::MouseMoved ||
type == sf::Event::MouseButtonPressed ||
type == sf::Event::MouseButtonReleased) {
simulated_mouse_pos = sf::Vector2i(x, y);
}
sf::Event event;
event.type = type;
switch (type) {
case sf::Event::MouseMoved:
event.mouseMove.x = x;
@ -145,7 +130,7 @@ void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y
default:
break;
}
engine->processEvent(event);
}
@ -234,22 +219,18 @@ PyObject* McRFPy_Automation::_screenshot(PyObject* self, PyObject* args) {
PyObject* McRFPy_Automation::_position(PyObject* self, PyObject* args) {
auto engine = getGameEngine();
if (!engine || !engine->getRenderTargetPtr()) {
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
return Py_BuildValue("(ii)", 0, 0);
}
// In headless mode, return the simulated mouse position (#111)
if (engine->isHeadless()) {
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
}
// In windowed mode, return the actual mouse position relative to window
// In headless mode, we'd need to track the simulated mouse position
// For now, return the actual mouse position relative to window if available
if (auto* window = dynamic_cast<sf::RenderWindow*>(engine->getRenderTargetPtr())) {
sf::Vector2i pos = sf::Mouse::getPosition(*window);
return Py_BuildValue("(ii)", pos.x, pos.y);
}
// Fallback to simulated position
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
// In headless mode, return simulated position (TODO: track this)
return Py_BuildValue("(ii)", 0, 0);
}
// Get screen size

View File

@ -51,12 +51,6 @@ public:
static sf::Keyboard::Key stringToKey(const std::string& keyName);
static void sleep_ms(int milliseconds);
// #111 - Simulated mouse position for headless mode
static sf::Vector2i getSimulatedMousePosition();
private:
static GameEngine* getGameEngine();
// #111 - Track simulated mouse position for headless mode
static sf::Vector2i simulated_mouse_pos;
};

View File

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

View File

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

View File

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

View File

@ -28,13 +28,6 @@ struct McRogueFaceConfig {
// Screenshot functionality for headless mode
std::string screenshot_path;
bool take_screenshot = false;
// Auto-exit when no timers remain (for --headless --exec automation)
bool auto_exit_after_exec = false;
// Exception handling: exit on first Python callback exception (default: true)
// Use --continue-after-exceptions to disable
bool exit_on_exception = true;
};
#endif // MCROGUEFACE_CONFIG_H

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include "UIDrawable.h"
#include "UIFrame.h"
#include "UICaption.h"
@ -19,31 +18,19 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds
}
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr};
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", nullptr};
const char* property_name;
PyObject* target_value;
float duration;
const char* easing_name = "linear";
int delta = 0;
PyObject* callback = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_name, &delta, &callback)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_name, &delta)) {
return -1;
}
// Validate callback is callable if provided
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
PyErr_SetString(PyExc_TypeError, "callback must be callable");
return -1;
}
// Convert None to nullptr for C++
if (callback == Py_None) {
callback = nullptr;
}
// Convert Python target value to AnimationValue
AnimationValue animValue;
@ -103,7 +90,7 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
// Create the Animation
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0);
return 0;
}
@ -139,70 +126,50 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
return NULL;
}
// Get type objects from the module to ensure they're initialized
PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
PyObject* caption_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
PyObject* sprite_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
PyObject* grid_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
PyObject* entity_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
// Get the UIDrawable from the Python object
UIDrawable* drawable = nullptr;
bool handled = false;
// Check type by comparing type names
const char* type_name = Py_TYPE(target_obj)->tp_name;
// Use PyObject_IsInstance to support inheritance
if (frame_type && PyObject_IsInstance(target_obj, frame_type)) {
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
if (frame->data) {
self->data->start(frame->data);
AnimationManager::getInstance().addAnimation(self->data);
handled = true;
}
drawable = frame->data.get();
}
else if (caption_type && PyObject_IsInstance(target_obj, caption_type)) {
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
if (caption->data) {
self->data->start(caption->data);
AnimationManager::getInstance().addAnimation(self->data);
handled = true;
}
drawable = caption->data.get();
}
else if (sprite_type && PyObject_IsInstance(target_obj, sprite_type)) {
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
if (sprite->data) {
self->data->start(sprite->data);
AnimationManager::getInstance().addAnimation(self->data);
handled = true;
}
drawable = sprite->data.get();
}
else if (grid_type && PyObject_IsInstance(target_obj, grid_type)) {
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
if (grid->data) {
self->data->start(grid->data);
AnimationManager::getInstance().addAnimation(self->data);
handled = true;
}
drawable = grid->data.get();
}
else if (entity_type && PyObject_IsInstance(target_obj, entity_type)) {
else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
// Special handling for Entity since it doesn't inherit from UIDrawable
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
if (entity->data) {
self->data->startEntity(entity->data);
AnimationManager::getInstance().addAnimation(self->data);
handled = true;
}
// Start the animation directly on the entity
self->data->startEntity(entity->data.get());
// Add to AnimationManager
AnimationManager::getInstance().addAnimation(self->data);
Py_RETURN_NONE;
}
// Clean up references
Py_XDECREF(frame_type);
Py_XDECREF(caption_type);
Py_XDECREF(sprite_type);
Py_XDECREF(grid_type);
Py_XDECREF(entity_type);
if (!handled) {
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity (or a subclass of these)");
else {
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
return NULL;
}
// Start the animation
self->data->start(drawable);
// Add to AnimationManager
AnimationManager::getInstance().addAnimation(self->data);
Py_RETURN_NONE;
}
@ -247,73 +214,21 @@ PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args
}, value);
}
PyObject* PyAnimation::complete(PyAnimationObject* self, PyObject* args) {
if (self->data) {
self->data->complete();
}
Py_RETURN_NONE;
}
PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) {
if (self->data && self->data->hasValidTarget()) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
}
PyGetSetDef PyAnimation::getsetters[] = {
{"property", (getter)get_property, NULL,
MCRF_PROPERTY(property, "Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index')."), NULL},
{"duration", (getter)get_duration, NULL,
MCRF_PROPERTY(duration, "Animation duration in seconds (float, read-only). Total time for the animation to complete."), NULL},
{"elapsed", (getter)get_elapsed, NULL,
MCRF_PROPERTY(elapsed, "Elapsed time in seconds (float, read-only). Time since the animation started."), NULL},
{"is_complete", (getter)get_is_complete, NULL,
MCRF_PROPERTY(is_complete, "Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called."), NULL},
{"is_delta", (getter)get_is_delta, NULL,
MCRF_PROPERTY(is_delta, "Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value."), NULL},
{"property", (getter)get_property, NULL, "Target property name", NULL},
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
{"elapsed", (getter)get_elapsed, NULL, "Elapsed time in seconds", NULL},
{"is_complete", (getter)get_is_complete, NULL, "Whether animation is complete", NULL},
{"is_delta", (getter)get_is_delta, NULL, "Whether animation uses delta mode", NULL},
{NULL}
};
PyMethodDef PyAnimation::methods[] = {
{"start", (PyCFunction)start, METH_VARARGS,
MCRF_METHOD(Animation, start,
MCRF_SIG("(target: UIDrawable)", "None"),
MCRF_DESC("Start the animation on a target UI element."),
MCRF_ARGS_START
MCRF_ARG("target", "The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)")
MCRF_RETURNS("None")
MCRF_NOTE("The animation will automatically stop if the target is destroyed. Call AnimationManager.update(delta_time) each frame to progress animations.")
)},
"Start the animation on a target UIDrawable"},
{"update", (PyCFunction)update, METH_VARARGS,
MCRF_METHOD(Animation, update,
MCRF_SIG("(delta_time: float)", "bool"),
MCRF_DESC("Update the animation by the given time delta."),
MCRF_ARGS_START
MCRF_ARG("delta_time", "Time elapsed since last update in seconds")
MCRF_RETURNS("bool: True if animation is still running, False if complete")
MCRF_NOTE("Typically called by AnimationManager automatically. Manual calls only needed for custom animation control.")
)},
"Update the animation by deltaTime (returns True if still running)"},
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
MCRF_METHOD(Animation, get_current_value,
MCRF_SIG("()", "Any"),
MCRF_DESC("Get the current interpolated value of the animation."),
MCRF_RETURNS("Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str)")
MCRF_NOTE("Return type matches the target property type. For sprite_index returns int, for pos returns (x, y), for fill_color returns (r, g, b, a).")
)},
{"complete", (PyCFunction)complete, METH_NOARGS,
MCRF_METHOD(Animation, complete,
MCRF_SIG("()", "None"),
MCRF_DESC("Complete the animation immediately by jumping to the final value."),
MCRF_RETURNS("None")
MCRF_NOTE("Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.")
)},
{"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS,
MCRF_METHOD(Animation, hasValidTarget,
MCRF_SIG("()", "bool"),
MCRF_DESC("Check if the animation still has a valid target."),
MCRF_RETURNS("bool: True if the target still exists, False if it was destroyed")
MCRF_NOTE("Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed.")
)},
"Get the current interpolated value"},
{NULL}
};

View File

@ -28,8 +28,6 @@ public:
static PyObject* start(PyAnimationObject* self, PyObject* args);
static PyObject* update(PyAnimationObject* self, PyObject* args);
static PyObject* get_current_value(PyAnimationObject* self, PyObject* args);
static PyObject* complete(PyAnimationObject* self, PyObject* args);
static PyObject* has_valid_target(PyAnimationObject* self, PyObject* args);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];

View File

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

View File

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

View File

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

View File

@ -28,13 +28,7 @@ public:
static PyObject* get_member(PyObject*, void*);
static int set_member(PyObject*, PyObject*, void*);
// Color helper methods
static PyObject* from_hex(PyObject* cls, PyObject* args);
static PyObject* to_hex(PyColorObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* lerp(PyColorObject* self, PyObject* args);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
static PyColorObject* from_arg(PyObject*);
};
@ -48,7 +42,6 @@ namespace mcrfpydef {
.tp_hash = PyColor::hash,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Color Object"),
.tp_methods = PyColor::methods,
.tp_getset = PyColor::getsetters,
.tp_init = (initproc)PyColor::init,
.tp_new = PyColor::pynew,

View File

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

View File

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

View File

@ -1,6 +1,5 @@
#include "PyFont.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
PyFont::PyFont(std::string filename)
@ -74,9 +73,7 @@ PyObject* PyFont::get_source(PyFontObject* self, void* closure)
}
PyGetSetDef PyFont::getsetters[] = {
{"family", (getter)PyFont::get_family, NULL,
MCRF_PROPERTY(family, "Font family name (str, read-only). Retrieved from font metadata."), NULL},
{"source", (getter)PyFont::get_source, NULL,
MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this font."), NULL},
{"family", (getter)PyFont::get_family, NULL, "Font family name", NULL},
{"source", (getter)PyFont::get_source, NULL, "Source filename of the font", NULL},
{NULL} // Sentinel
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,16 +15,9 @@ private:
sf::Texture texture;
std::string source;
int sheet_width, sheet_height;
// Private default constructor for factory methods
PyTexture() : source("<uninitialized>"), sprite_width(0), sprite_height(0), sheet_width(0), sheet_height(0) {}
public:
int sprite_width, sprite_height; // just use them read only, OK?
PyTexture(std::string filename, int sprite_w, int sprite_h);
// #144: Factory method to create texture from rendered content (snapshot)
static std::shared_ptr<PyTexture> from_rendered(sf::RenderTexture& render_tex);
sf::Sprite sprite(int index, sf::Vector2f pos = sf::Vector2f(0, 0), sf::Vector2f s = sf::Vector2f(1.0, 1.0));
int getSpriteCount() const { return sheet_width * sheet_height; }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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