Compare commits
5 Commits
master
...
alpha_stre
| Author | SHA1 | Date |
|---|---|---|
|
|
99f301e3a0 | |
|
|
2f2b488fb5 | |
|
|
5a003a9aa5 | |
|
|
e5affaf317 | |
|
|
d03182d347 |
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
|
@ -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)
|
||||
|
|
@ -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_*
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
[submodule "modules/SFML"]
|
||||
path = modules/SFML
|
||||
url = git@github.com:SFML/SFML.git
|
||||
[submodule "modules/libtcod-headless"]
|
||||
path = modules/libtcod-headless
|
||||
url = git@github.com:jmccardle/libtcod-headless.git
|
||||
branch = 2.2.1-headless
|
||||
[submodule "modules/libtcod"]
|
||||
path = modules/libtcod
|
||||
url = git@github.com:libtcod/libtcod.git
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"gitea": {
|
||||
"type": "stdio",
|
||||
"command": "/home/john/Development/discord_for_claude/forgejo-mcp.linux.amd64",
|
||||
"args": ["stdio", "--server", "https://gamedev.ffwf.net/gitea/", "--token", "f58ec698a5edee82db4960920b13d3f7d0d58d8e"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
# Building McRogueFace from Source
|
||||
|
||||
This document describes how to build McRogueFace from a fresh clone.
|
||||
|
||||
## Build Options
|
||||
|
||||
There are two ways to build McRogueFace:
|
||||
|
||||
1. **Quick Build** (recommended): Use pre-built dependency libraries from a `build_deps` archive
|
||||
2. **Full Build**: Compile all dependencies from submodules
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Dependencies
|
||||
|
||||
Install these packages before building:
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install \
|
||||
build-essential \
|
||||
cmake \
|
||||
git \
|
||||
zlib1g-dev \
|
||||
libx11-dev \
|
||||
libxrandr-dev \
|
||||
libxcursor-dev \
|
||||
libfreetype-dev \
|
||||
libudev-dev \
|
||||
libvorbis-dev \
|
||||
libflac-dev \
|
||||
libgl-dev \
|
||||
libopenal-dev
|
||||
```
|
||||
|
||||
**Note:** SDL is NOT required - McRogueFace uses libtcod-headless which has no SDL dependency.
|
||||
|
||||
---
|
||||
|
||||
## Option 1: Quick Build (Using Pre-built Dependencies)
|
||||
|
||||
If you have a `build_deps.tar.gz` or `build_deps.zip` archive:
|
||||
|
||||
```bash
|
||||
# Clone McRogueFace (no submodules needed)
|
||||
git clone <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
|
||||
# Extract pre-built dependencies
|
||||
tar -xzf /path/to/build_deps.tar.gz
|
||||
# Or for zip: unzip /path/to/build_deps.zip
|
||||
|
||||
# Build McRogueFace
|
||||
mkdir -p build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# Run
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
The `build_deps` archive contains:
|
||||
- `__lib/` - Pre-built shared libraries (Python, SFML, libtcod-headless)
|
||||
- `deps/` - Header symlinks for compilation
|
||||
|
||||
**Total build time: ~30 seconds**
|
||||
|
||||
---
|
||||
|
||||
## Option 2: Full Build (Compiling All Dependencies)
|
||||
|
||||
### 1. Clone with Submodules
|
||||
|
||||
```bash
|
||||
git clone --recursive <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
```
|
||||
|
||||
If submodules weren't cloned:
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
**Note:** imgui/imgui-sfml submodules may fail - this is fine, they're not used.
|
||||
|
||||
### 2. Create Dependency Symlinks
|
||||
|
||||
```bash
|
||||
cd deps
|
||||
ln -sf ../modules/cpython cpython
|
||||
ln -sf ../modules/libtcod-headless/src/libtcod libtcod
|
||||
ln -sf ../modules/cpython/Include Python
|
||||
ln -sf ../modules/SFML/include/SFML SFML
|
||||
cd ..
|
||||
```
|
||||
|
||||
### 3. Build libtcod-headless
|
||||
|
||||
libtcod-headless is our SDL-free fork with vendored dependencies:
|
||||
|
||||
```bash
|
||||
cd modules/libtcod-headless
|
||||
mkdir build && cd build
|
||||
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=ON
|
||||
|
||||
make -j$(nproc)
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
That's it! No special flags needed - libtcod-headless defaults to:
|
||||
- `LIBTCOD_SDL3=disable` (no SDL dependency)
|
||||
- Vendored lodepng, utf8proc, stb
|
||||
|
||||
### 4. Build Python 3.12
|
||||
|
||||
```bash
|
||||
cd modules/cpython
|
||||
./configure --enable-shared
|
||||
make -j$(nproc)
|
||||
cd ../..
|
||||
```
|
||||
|
||||
### 5. Build SFML 2.6
|
||||
|
||||
```bash
|
||||
cd modules/SFML
|
||||
mkdir build && cd build
|
||||
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=ON
|
||||
|
||||
make -j$(nproc)
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
### 6. Copy Libraries
|
||||
|
||||
```bash
|
||||
mkdir -p __lib
|
||||
|
||||
# Python
|
||||
cp modules/cpython/libpython3.12.so* __lib/
|
||||
|
||||
# SFML
|
||||
cp modules/SFML/build/lib/libsfml-*.so* __lib/
|
||||
|
||||
# libtcod-headless
|
||||
cp modules/libtcod-headless/build/bin/libtcod.so* __lib/
|
||||
|
||||
# Python standard library
|
||||
cp -r modules/cpython/Lib __lib/Python
|
||||
```
|
||||
|
||||
### 7. Build McRogueFace
|
||||
|
||||
```bash
|
||||
mkdir -p build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
### 8. Run
|
||||
|
||||
```bash
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Submodule Versions
|
||||
|
||||
| Submodule | Version | Notes |
|
||||
|-----------|---------|-------|
|
||||
| SFML | 2.6.1 | Graphics, audio, windowing |
|
||||
| cpython | 3.12.2 | Embedded Python interpreter |
|
||||
| libtcod-headless | 2.2.1 | SDL-free fork for FOV, pathfinding |
|
||||
|
||||
---
|
||||
|
||||
## Creating a build_deps Archive
|
||||
|
||||
To create a `build_deps` archive for distribution:
|
||||
|
||||
```bash
|
||||
cd McRogueFace
|
||||
|
||||
# Create archive directory
|
||||
mkdir -p build_deps_staging
|
||||
|
||||
# Copy libraries
|
||||
cp -r __lib build_deps_staging/
|
||||
|
||||
# Copy/create deps symlinks as actual directories with only needed headers
|
||||
mkdir -p build_deps_staging/deps
|
||||
cp -rL deps/libtcod build_deps_staging/deps/ # Follow symlink
|
||||
cp -rL deps/Python build_deps_staging/deps/
|
||||
cp -rL deps/SFML build_deps_staging/deps/
|
||||
cp -r deps/platform build_deps_staging/deps/
|
||||
|
||||
# Create archives
|
||||
cd build_deps_staging
|
||||
tar -czf ../build_deps.tar.gz __lib deps
|
||||
zip -r ../build_deps.zip __lib deps
|
||||
cd ..
|
||||
|
||||
# Cleanup
|
||||
rm -rf build_deps_staging
|
||||
```
|
||||
|
||||
The resulting archive can be distributed alongside releases for users who want to build McRogueFace without compiling dependencies.
|
||||
|
||||
**Archive contents:**
|
||||
```
|
||||
build_deps.tar.gz
|
||||
├── __lib/
|
||||
│ ├── libpython3.12.so*
|
||||
│ ├── libsfml-*.so*
|
||||
│ ├── libtcod.so*
|
||||
│ └── Python/ # Python standard library
|
||||
└── deps/
|
||||
├── libtcod/ # libtcod headers
|
||||
├── Python/ # Python headers
|
||||
├── SFML/ # SFML headers
|
||||
└── platform/ # Platform-specific configs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verify the Build
|
||||
|
||||
```bash
|
||||
cd build
|
||||
|
||||
# Check version
|
||||
./mcrogueface --version
|
||||
|
||||
# Test headless mode
|
||||
./mcrogueface --headless -c "import mcrfpy; print('Success')"
|
||||
|
||||
# Verify no SDL dependencies
|
||||
ldd mcrogueface | grep -i sdl # Should output nothing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### OpenAL not found
|
||||
```bash
|
||||
sudo apt install libopenal-dev
|
||||
```
|
||||
|
||||
### FreeType not found
|
||||
```bash
|
||||
sudo apt install libfreetype-dev
|
||||
```
|
||||
|
||||
### X11/Xrandr not found
|
||||
```bash
|
||||
sudo apt install libx11-dev libxrandr-dev
|
||||
```
|
||||
|
||||
### Python standard library missing
|
||||
Ensure `__lib/Python` contains the standard library:
|
||||
```bash
|
||||
ls __lib/Python/os.py # Should exist
|
||||
```
|
||||
|
||||
### libtcod symbols not found
|
||||
Ensure libtcod.so is in `__lib/` with correct version:
|
||||
```bash
|
||||
ls -la __lib/libtcod.so*
|
||||
# Should show libtcod.so -> libtcod.so.2 -> libtcod.so.2.2.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Times (approximate)
|
||||
|
||||
On a typical 4-core system:
|
||||
|
||||
| Component | Time |
|
||||
|-----------|------|
|
||||
| libtcod-headless | ~30 seconds |
|
||||
| Python 3.12 | ~3-5 minutes |
|
||||
| SFML 2.6 | ~1 minute |
|
||||
| McRogueFace | ~30 seconds |
|
||||
| **Full build total** | **~5-7 minutes** |
|
||||
| **Quick build (pre-built deps)** | **~30 seconds** |
|
||||
|
||||
---
|
||||
|
||||
## Runtime Dependencies
|
||||
|
||||
The built executable requires these system libraries:
|
||||
- `libz.so.1` (zlib)
|
||||
- `libopenal.so.1` (OpenAL)
|
||||
- `libX11.so.6`, `libXrandr.so.2` (X11)
|
||||
- `libfreetype.so.6` (FreeType)
|
||||
- `libGL.so.1` (OpenGL)
|
||||
|
||||
All other dependencies (Python, SFML, libtcod) are bundled in `lib/`.
|
||||
626
CLAUDE.md
|
|
@ -1,626 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Gitea-First Workflow
|
||||
|
||||
**IMPORTANT**: This project uses Gitea for issue tracking, documentation, and project management. Always consult and update Gitea resources before and during development work.
|
||||
|
||||
**Gitea Instance**: https://gamedev.ffwf.net/gitea/john/McRogueFace
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Gitea is the Single Source of Truth**
|
||||
- Issue tracker contains current tasks, bugs, and feature requests
|
||||
- Wiki contains living documentation and architecture decisions
|
||||
- Use Gitea MCP tools to query and update issues programmatically
|
||||
|
||||
2. **Always Check Gitea First**
|
||||
- Before starting work: Check open issues for related tasks or blockers
|
||||
- When using `/roadmap` command: Query Gitea for up-to-date issue status
|
||||
- When researching a feature: Search Gitea wiki and issues before grepping codebase
|
||||
- When encountering a bug: Check if an issue already exists
|
||||
|
||||
3. **Create Granular Issues**
|
||||
- Break large features into separate, focused issues
|
||||
- Each issue should address one specific problem or enhancement
|
||||
- Tag issues appropriately: `[Bugfix]`, `[Major Feature]`, `[Minor Feature]`, etc.
|
||||
- Link related issues using dependencies or blocking relationships
|
||||
|
||||
4. **Document as You Go**
|
||||
- When work on one issue interacts with another system: Add notes to related issues
|
||||
- When discovering undocumented behavior: Create task to document it
|
||||
- When documentation misleads you: Create task to correct or expand it
|
||||
- When implementing a feature: Update the Gitea wiki if appropriate
|
||||
|
||||
5. **Cross-Reference Everything**
|
||||
- Commit messages should reference issue numbers (e.g., "Fixes #104", "Addresses #125")
|
||||
- Issue comments should link to commits when work is done
|
||||
- Wiki pages should reference relevant issues for implementation details
|
||||
- Issues should link to each other when dependencies exist
|
||||
|
||||
### Workflow Pattern
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 1. Check Gitea Issues & Wiki │
|
||||
│ - Is there an existing issue for this? │
|
||||
│ - What's the current status? │
|
||||
│ - Are there related issues or blockers? │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 2. Create Issues (if needed) │
|
||||
│ - Break work into granular tasks │
|
||||
│ - Tag appropriately │
|
||||
│ - Link dependencies │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 3. Do the Work │
|
||||
│ - Implement/fix/document │
|
||||
│ - Write tests first (TDD) │
|
||||
│ - Add inline documentation │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 4. Update Gitea │
|
||||
│ - Add notes to affected issues │
|
||||
│ - Create follow-up issues for discovered work │
|
||||
│ - Update wiki if architecture/APIs changed │
|
||||
│ - Add documentation correction tasks │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 5. Commit & Reference │
|
||||
│ - Commit messages reference issue numbers │
|
||||
│ - Close issues or update status │
|
||||
│ - Add commit links to issue comments │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Benefits of Gitea-First Approach
|
||||
|
||||
- **Reduced Context Switching**: Check brief issue descriptions instead of re-reading entire codebase
|
||||
- **Better Planning**: Issues provide roadmap; avoid duplicate or contradictory work
|
||||
- **Living Documentation**: Wiki and issues stay current as work progresses
|
||||
- **Historical Context**: Issue comments capture why decisions were made
|
||||
- **Efficiency**: MCP tools allow programmatic access to project state
|
||||
|
||||
### MCP Tools Available
|
||||
|
||||
Claude Code has access to Gitea MCP tools for:
|
||||
- `list_repo_issues` - Query current issues with filtering
|
||||
- `get_issue` - Get detailed issue information
|
||||
- `create_issue` - Create new issues programmatically
|
||||
- `create_issue_comment` - Add comments to issues
|
||||
- `edit_issue` - Update issue status, title, body
|
||||
- `add_issue_labels` - Tag issues appropriately
|
||||
- `add_issue_dependency` / `add_issue_blocking` - Link related issues
|
||||
- Plus wiki, milestone, and label management tools
|
||||
|
||||
Use these tools liberally to keep the project organized!
|
||||
|
||||
### Gitea Label System
|
||||
|
||||
**IMPORTANT**: Always apply appropriate labels when creating new issues!
|
||||
|
||||
The project uses a structured label system to organize issues:
|
||||
|
||||
**Label Categories:**
|
||||
|
||||
1. **System Labels** (identify affected codebase area):
|
||||
- `system:rendering` - Rendering pipeline and visuals
|
||||
- `system:ui-hierarchy` - UI component hierarchy and composition
|
||||
- `system:grid` - Grid system and spatial containers
|
||||
- `system:animation` - Animation and property interpolation
|
||||
- `system:python-binding` - Python/C++ binding layer
|
||||
- `system:input` - Input handling and events
|
||||
- `system:performance` - Performance optimization and profiling
|
||||
- `system:documentation` - Documentation infrastructure
|
||||
|
||||
2. **Priority Labels** (development timeline):
|
||||
- `priority:tier1-active` - Current development focus - critical path to v1.0
|
||||
- `priority:tier2-foundation` - Important foundation work - not blocking v1.0
|
||||
- `priority:tier3-future` - Future features - deferred until after v1.0
|
||||
|
||||
3. **Type/Scope Labels** (effort and complexity):
|
||||
- `Major Feature` - Significant time and effort required
|
||||
- `Minor Feature` - Some effort required to create or overhaul functionality
|
||||
- `Tiny Feature` - Quick and easy - a few lines or little interconnection
|
||||
- `Bugfix` - Fixes incorrect behavior
|
||||
- `Refactoring & Cleanup` - No new functionality, just improving codebase
|
||||
- `Documentation` - Documentation work
|
||||
- `Demo Target` - Functionality to demonstrate
|
||||
|
||||
4. **Workflow Labels** (current blockers/needs):
|
||||
- `workflow:blocked` - Blocked by other work - waiting on dependencies
|
||||
- `workflow:needs-documentation` - Needs documentation before or after implementation
|
||||
- `workflow:needs-benchmark` - Needs performance testing and benchmarks
|
||||
- `Alpha Release Requirement` - Blocker to 0.1 Alpha release
|
||||
|
||||
**When creating issues:**
|
||||
- Apply at least one `system:*` label (what part of codebase)
|
||||
- Apply one `priority:tier*` label (when to address it)
|
||||
- Apply one type label (`Major Feature`, `Minor Feature`, `Tiny Feature`, or `Bugfix`)
|
||||
- Apply `workflow:*` labels if applicable (blocked, needs docs, needs benchmarks)
|
||||
|
||||
**Example label combinations:**
|
||||
- New rendering feature: `system:rendering`, `priority:tier2-foundation`, `Major Feature`
|
||||
- Python API improvement: `system:python-binding`, `priority:tier1-active`, `Minor Feature`
|
||||
- Performance work: `system:performance`, `priority:tier1-active`, `Major Feature`, `workflow:needs-benchmark`
|
||||
|
||||
**⚠️ CRITICAL BUG**: The Gitea MCP tool (v0.07) has a label application bug documented in `GITEA_MCP_LABEL_BUG_REPORT.md`:
|
||||
- `add_issue_labels` and `replace_issue_labels` behave inconsistently
|
||||
- Single ID arrays produce different results than multi-ID arrays for the SAME IDs
|
||||
- Label IDs do not map reliably to actual labels
|
||||
|
||||
**Workaround Options:**
|
||||
1. **Best**: Apply labels manually via web interface: `https://gamedev.ffwf.net/gitea/john/McRogueFace/issues/<number>`
|
||||
2. **Automated**: Apply labels ONE AT A TIME using single-element arrays (slower but more reliable)
|
||||
3. **Use single-ID mapping** (documented below)
|
||||
|
||||
**Label ID Reference** (for documentation purposes - see issue #131 for details):
|
||||
```
|
||||
1=Major Feature, 2=Alpha Release, 3=Bugfix, 4=Demo Target, 5=Documentation,
|
||||
6=Minor Feature, 7=tier1-active, 8=tier2-foundation, 9=tier3-future,
|
||||
10=Refactoring, 11=animation, 12=docs, 13=grid, 14=input, 15=performance,
|
||||
16=python-binding, 17=rendering, 18=ui-hierarchy, 19=Tiny Feature,
|
||||
20=blocked, 21=needs-benchmark, 22=needs-documentation
|
||||
```
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Build the project (compiles to ./build directory)
|
||||
make
|
||||
|
||||
# Or use the build script directly
|
||||
./build.sh
|
||||
|
||||
# Run the game
|
||||
make run
|
||||
|
||||
# Clean build artifacts
|
||||
make clean
|
||||
|
||||
# The executable and all assets are in ./build/
|
||||
cd build
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
## Project Architecture
|
||||
|
||||
McRogueFace is a C++ game engine with Python scripting support, designed for creating roguelike games. The architecture consists of:
|
||||
|
||||
### Core Engine (C++)
|
||||
- **Entry Point**: `src/main.cpp` initializes the game engine
|
||||
- **Scene System**: `Scene.h/cpp` manages game states
|
||||
- **Entity System**: `UIEntity.h/cpp` provides game objects
|
||||
- **Python Integration**: `McRFPy_API.h/cpp` exposes engine functionality to Python
|
||||
- **UI Components**: `UIFrame`, `UICaption`, `UISprite`, `UIGrid` for rendering
|
||||
|
||||
### Game Logic (Python)
|
||||
- **Main Script**: `src/scripts/game.py` contains game initialization and scene setup
|
||||
- **Entity System**: `src/scripts/cos_entities.py` implements game entities (Player, Enemy, Boulder, etc.)
|
||||
- **Level Generation**: `src/scripts/cos_level.py` uses BSP for procedural dungeon generation
|
||||
- **Tile System**: `src/scripts/cos_tiles.py` implements Wave Function Collapse for tile placement
|
||||
|
||||
### Key Python API (`mcrfpy` module)
|
||||
The C++ engine exposes these primary functions to Python:
|
||||
- Scene Management: `createScene()`, `setScene()`, `sceneUI()`
|
||||
- Entity Creation: `Entity()` with position and sprite properties
|
||||
- Grid Management: `Grid()` for tilemap rendering
|
||||
- Input Handling: `keypressScene()` for keyboard events
|
||||
- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()`
|
||||
- Timers: `setTimer()`, `delTimer()` for event scheduling
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Running the Game
|
||||
After building, the executable expects:
|
||||
- `assets/` directory with sprites, fonts, and audio
|
||||
- `scripts/` directory with Python game files
|
||||
- Python 3.12 shared libraries in `./lib/`
|
||||
|
||||
### Modifying Game Logic
|
||||
- Game scripts are in `src/scripts/`
|
||||
- Main game entry is `game.py`
|
||||
- Entity behavior in `cos_entities.py`
|
||||
- Level generation in `cos_level.py`
|
||||
|
||||
### Adding New Features
|
||||
1. C++ API additions go in `src/McRFPy_API.cpp`
|
||||
2. Expose to Python using the existing binding pattern
|
||||
3. Update Python scripts to use new functionality
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Suite Structure
|
||||
|
||||
The `tests/` directory contains the comprehensive test suite:
|
||||
|
||||
```
|
||||
tests/
|
||||
├── run_tests.py # Test runner - executes all tests with timeout
|
||||
├── unit/ # Unit tests for individual components (105+ tests)
|
||||
├── integration/ # Integration tests for system interactions
|
||||
├── regression/ # Bug regression tests (issue_XX_*.py)
|
||||
├── benchmarks/ # Performance benchmarks
|
||||
├── demo/ # Feature demonstration system
|
||||
│ ├── demo_main.py # Interactive demo runner
|
||||
│ ├── screens/ # Per-feature demo screens
|
||||
│ └── screenshots/ # Generated demo screenshots
|
||||
└── notes/ # Analysis files and documentation
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run the full test suite (from tests/ directory)
|
||||
cd tests && python3 run_tests.py
|
||||
|
||||
# Run a specific test
|
||||
cd build && ./mcrogueface --headless --exec ../tests/unit/some_test.py
|
||||
|
||||
# Run the demo system interactively
|
||||
cd build && ./mcrogueface ../tests/demo/demo_main.py
|
||||
|
||||
# Generate demo screenshots (headless)
|
||||
cd build && ./mcrogueface --headless --exec ../tests/demo/demo_main.py
|
||||
```
|
||||
|
||||
### Reading Tests as Examples
|
||||
|
||||
**IMPORTANT**: Before implementing a feature or fixing a bug, check existing tests for API usage examples:
|
||||
|
||||
- `tests/unit/` - Shows correct usage of individual mcrfpy classes and functions
|
||||
- `tests/demo/screens/` - Complete working examples of UI components
|
||||
- `tests/regression/` - Documents edge cases and bug scenarios
|
||||
|
||||
Example: To understand Animation API:
|
||||
```bash
|
||||
grep -r "Animation" tests/unit/
|
||||
cat tests/demo/screens/animation_demo.py
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
**Always write tests when adding features or fixing bugs:**
|
||||
|
||||
1. **For new features**: Create `tests/unit/feature_name_test.py`
|
||||
2. **For bug fixes**: Create `tests/regression/issue_XX_description_test.py`
|
||||
3. **For demos**: Add to `tests/demo/screens/` if it showcases a feature
|
||||
|
||||
### Quick Testing Commands
|
||||
```bash
|
||||
# Test headless mode with inline Python
|
||||
cd build
|
||||
./mcrogueface --headless -c "import mcrfpy; print('Headless test')"
|
||||
|
||||
# Run specific test with output
|
||||
./mcrogueface --headless --exec ../tests/unit/my_test.py 2>&1
|
||||
```
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Compiling McRogueFace
|
||||
```bash
|
||||
# Standard build (to ./build directory)
|
||||
make
|
||||
|
||||
# Full rebuild
|
||||
make clean && make
|
||||
|
||||
# Manual CMake build
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# The library path issue: if linking fails, check that libraries are in __lib/
|
||||
# CMakeLists.txt expects: link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
```
|
||||
|
||||
### Running and Capturing Output
|
||||
```bash
|
||||
# Run with timeout and capture output
|
||||
cd build
|
||||
timeout 5 ./mcrogueface 2>&1 | tee output.log
|
||||
|
||||
# Run in background and kill after delay
|
||||
./mcrogueface > output.txt 2>&1 & PID=$!; sleep 3; kill $PID 2>/dev/null
|
||||
|
||||
# Just capture first N lines (useful for crashes)
|
||||
./mcrogueface 2>&1 | head -50
|
||||
```
|
||||
|
||||
### Debugging with GDB
|
||||
```bash
|
||||
# Interactive debugging
|
||||
gdb ./mcrogueface
|
||||
(gdb) run
|
||||
(gdb) bt # backtrace after crash
|
||||
|
||||
# Batch mode debugging (non-interactive)
|
||||
gdb -batch -ex run -ex where -ex quit ./mcrogueface 2>&1
|
||||
|
||||
# Get just the backtrace after a crash
|
||||
gdb -batch -ex "run" -ex "bt" ./mcrogueface 2>&1 | head -50
|
||||
|
||||
# Debug with specific commands
|
||||
echo -e "run\nbt 5\nquit\ny" | gdb ./mcrogueface 2>&1
|
||||
```
|
||||
|
||||
### Testing Different Python Scripts
|
||||
```bash
|
||||
# The game automatically runs build/scripts/game.py on startup
|
||||
# To test different behavior:
|
||||
|
||||
# Option 1: Replace game.py temporarily
|
||||
cd build
|
||||
cp scripts/my_test_script.py scripts/game.py
|
||||
./mcrogueface
|
||||
|
||||
# Option 2: Backup original and test
|
||||
mv scripts/game.py scripts/game.py.bak
|
||||
cp my_test.py scripts/game.py
|
||||
./mcrogueface
|
||||
mv scripts/game.py.bak scripts/game.py
|
||||
|
||||
# Option 3: For quick tests, create minimal game.py
|
||||
echo 'import mcrfpy; print("Test"); mcrfpy.createScene("test")' > scripts/game.py
|
||||
```
|
||||
|
||||
### Understanding Key Macros and Patterns
|
||||
|
||||
#### RET_PY_INSTANCE Macro (UIDrawable.h)
|
||||
This macro handles converting C++ UI objects to their Python equivalents:
|
||||
```cpp
|
||||
RET_PY_INSTANCE(target);
|
||||
// Expands to a switch on target->derived_type() that:
|
||||
// 1. Allocates the correct Python object type (Frame, Caption, Sprite, Grid)
|
||||
// 2. Sets the shared_ptr data member
|
||||
// 3. Returns the PyObject*
|
||||
```
|
||||
|
||||
#### Collection Patterns
|
||||
- `UICollection` wraps `std::vector<std::shared_ptr<UIDrawable>>`
|
||||
- `UIEntityCollection` wraps `std::list<std::shared_ptr<UIEntity>>`
|
||||
- Different containers require different iteration code (vector vs list)
|
||||
|
||||
#### Python Object Creation Patterns
|
||||
```cpp
|
||||
// Pattern 1: Using tp_alloc (most common)
|
||||
auto o = (PyUIFrameObject*)type->tp_alloc(type, 0);
|
||||
o->data = std::make_shared<UIFrame>();
|
||||
|
||||
// Pattern 2: Getting type from module
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
||||
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
||||
|
||||
// Pattern 3: Direct shared_ptr assignment
|
||||
iterObj->data = self->data; // Shares the C++ object
|
||||
```
|
||||
|
||||
### Working Directory Structure
|
||||
```
|
||||
build/
|
||||
├── mcrogueface # The executable
|
||||
├── scripts/
|
||||
│ └── game.py # Auto-loaded Python script
|
||||
├── assets/ # Copied from source during build
|
||||
└── lib/ # Python libraries (copied from __lib/)
|
||||
```
|
||||
|
||||
### Quick Iteration Tips
|
||||
- Keep a test script ready for quick experiments
|
||||
- Use `timeout` to auto-kill hanging processes
|
||||
- The game expects a window manager; use Xvfb for headless testing
|
||||
- Python errors go to stderr, game output to stdout
|
||||
- Segfaults usually mean Python type initialization issues
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The project uses SFML for graphics/audio and libtcod for roguelike utilities
|
||||
- Python scripts are loaded at runtime from the `scripts/` directory
|
||||
- Asset loading expects specific paths relative to the executable
|
||||
- The game was created for 7DRL 2025 as "Crypt of Sokoban"
|
||||
- Iterator implementations require careful handling of C++/Python boundaries
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Test-Driven Development
|
||||
- **Always write tests first**: Create tests in `./tests/` for all bugs and new features
|
||||
- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix
|
||||
- **Read existing tests**: Check `tests/unit/` and `tests/demo/screens/` for API examples before writing code
|
||||
- **Close the loop**: Reproduce issue → change code → recompile → run test → verify
|
||||
|
||||
### Two Types of Tests
|
||||
|
||||
#### 1. Direct Execution Tests (No Game Loop)
|
||||
For tests that only need class initialization or direct code execution:
|
||||
```python
|
||||
# tests/unit/my_feature_test.py
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Test code - runs immediately
|
||||
frame = mcrfpy.Frame(pos=(0,0), size=(100,100))
|
||||
assert frame.x == 0
|
||||
assert frame.w == 100
|
||||
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
```
|
||||
|
||||
#### 2. Game Loop Tests (Timer-Based)
|
||||
For tests requiring rendering, screenshots, or elapsed time:
|
||||
```python
|
||||
# tests/unit/my_visual_test.py
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
def run_test(runtime):
|
||||
"""Timer callback - runs after game loop starts"""
|
||||
automation.screenshot("test_result.png")
|
||||
# Validate results...
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.createScene("test")
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
ui.append(mcrfpy.Frame(pos=(50,50), size=(100,100)))
|
||||
mcrfpy.setScene("test")
|
||||
mcrfpy.setTimer("test", run_test, 100)
|
||||
```
|
||||
|
||||
### Key Testing Principles
|
||||
- **Timer callbacks are essential**: Screenshots only work after the render loop starts
|
||||
- **Use automation API**: `automation.screenshot()`, `automation.click()` for visual testing
|
||||
- **Exit properly**: Always call `sys.exit(0)` for PASS or `sys.exit(1)` for FAIL
|
||||
- **Headless mode**: Use `--headless --exec` for CI/automated testing
|
||||
- **Check examples first**: Read `tests/demo/screens/*.py` for correct API usage
|
||||
|
||||
### API Quick Reference (from tests)
|
||||
```python
|
||||
# Animation: (property, target_value, duration, easing)
|
||||
anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut")
|
||||
anim.start(frame)
|
||||
|
||||
# Caption: use keyword arguments to avoid positional conflicts
|
||||
cap = mcrfpy.Caption(text="Hello", pos=(100, 100))
|
||||
|
||||
# Grid center: uses pixel coordinates, not cell coordinates
|
||||
grid = mcrfpy.Grid(grid_size=(15, 10), pos=(50, 50), size=(400, 300))
|
||||
grid.center = (120, 80) # pixels: (cells * cell_size / 2)
|
||||
|
||||
# Keyboard handler: key names are "Num1", "Num2", "Escape", "Q", etc.
|
||||
def on_key(key, state):
|
||||
if key == "Num1" and state == "start":
|
||||
mcrfpy.setScene("demo_1")
|
||||
```
|
||||
|
||||
## Development Best Practices
|
||||
|
||||
### Testing and Deployment
|
||||
- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, tests shouldn't be included
|
||||
- **Run full suite before commits**: `cd tests && python3 run_tests.py`
|
||||
|
||||
## Documentation Guidelines
|
||||
|
||||
### Documentation Macro System
|
||||
|
||||
**As of 2025-10-30, McRogueFace uses a macro-based documentation system** (`src/McRFPy_Doc.h`) that ensures consistent, complete docstrings across all Python bindings.
|
||||
|
||||
#### Include the Header
|
||||
|
||||
```cpp
|
||||
#include "McRFPy_Doc.h"
|
||||
```
|
||||
|
||||
#### Documenting Methods
|
||||
|
||||
For methods in PyMethodDef arrays, use `MCRF_METHOD`:
|
||||
|
||||
```cpp
|
||||
{"method_name", (PyCFunction)Class::method, METH_VARARGS,
|
||||
MCRF_METHOD(ClassName, method_name,
|
||||
MCRF_SIG("(arg1: type, arg2: type)", "return_type"),
|
||||
MCRF_DESC("Brief description of what the method does."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("arg1", "Description of first argument")
|
||||
MCRF_ARG("arg2", "Description of second argument")
|
||||
MCRF_RETURNS("Description of return value")
|
||||
MCRF_RAISES("ValueError", "Condition that raises this exception")
|
||||
MCRF_NOTE("Important notes or caveats")
|
||||
MCRF_LINK("docs/guide.md", "Related Documentation")
|
||||
)},
|
||||
```
|
||||
|
||||
#### Documenting Properties
|
||||
|
||||
For properties in PyGetSetDef arrays, use `MCRF_PROPERTY`:
|
||||
|
||||
```cpp
|
||||
{"property_name", (getter)getter_func, (setter)setter_func,
|
||||
MCRF_PROPERTY(property_name,
|
||||
"Brief description of the property. "
|
||||
"Additional details about valid values, side effects, etc."
|
||||
), NULL},
|
||||
```
|
||||
|
||||
#### Available Macros
|
||||
|
||||
- `MCRF_SIG(params, ret)` - Method signature
|
||||
- `MCRF_DESC(text)` - Description paragraph
|
||||
- `MCRF_ARGS_START` - Begin arguments section
|
||||
- `MCRF_ARG(name, desc)` - Individual argument
|
||||
- `MCRF_RETURNS(text)` - Return value description
|
||||
- `MCRF_RAISES(exception, condition)` - Exception documentation
|
||||
- `MCRF_NOTE(text)` - Important notes
|
||||
- `MCRF_LINK(path, text)` - Reference to external documentation
|
||||
|
||||
#### Documentation Prose Guidelines
|
||||
|
||||
**Keep C++ docstrings concise** (1-2 sentences per section). For complex topics, use `MCRF_LINK` to reference external guides:
|
||||
|
||||
```cpp
|
||||
MCRF_LINK("docs/animation-guide.md", "Animation System Tutorial")
|
||||
```
|
||||
|
||||
**External documentation** (in `docs/`) can be verbose with examples, tutorials, and design rationale.
|
||||
|
||||
### Regenerating Documentation
|
||||
|
||||
After modifying C++ inline documentation with MCRF_* macros:
|
||||
|
||||
1. **Rebuild the project**: `make -j$(nproc)`
|
||||
|
||||
2. **Generate all documentation** (recommended - single command):
|
||||
```bash
|
||||
./tools/generate_all_docs.sh
|
||||
```
|
||||
This creates:
|
||||
- `docs/api_reference_dynamic.html` - HTML API reference
|
||||
- `docs/API_REFERENCE_DYNAMIC.md` - Markdown API reference
|
||||
- `docs/mcrfpy.3` - Unix man page (section 3)
|
||||
- `stubs/mcrfpy.pyi` - Type stubs for IDE support
|
||||
|
||||
3. **Or generate individually**:
|
||||
```bash
|
||||
# API docs (HTML + Markdown)
|
||||
./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py
|
||||
|
||||
# Type stubs (manually-maintained with @overload support)
|
||||
./build/mcrogueface --headless --exec tools/generate_stubs_v2.py
|
||||
|
||||
# Man page (requires pandoc)
|
||||
./tools/generate_man_page.sh
|
||||
```
|
||||
|
||||
**System Requirements:**
|
||||
- `pandoc` must be installed for man page generation: `sudo apt-get install pandoc`
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Single source of truth**: Documentation lives in C++ source files via MCRF_* macros
|
||||
- **McRogueFace as Python interpreter**: Documentation scripts MUST be run using McRogueFace itself, not system Python
|
||||
- **Use --headless --exec**: For non-interactive documentation generation
|
||||
- **Link transformation**: `MCRF_LINK` references are transformed to appropriate format (HTML, Markdown, etc.)
|
||||
- **No manual dictionaries**: The old hardcoded documentation system has been removed
|
||||
|
||||
### Documentation Pipeline Architecture
|
||||
|
||||
1. **C++ Source** → MCRF_* macros in PyMethodDef/PyGetSetDef arrays
|
||||
2. **Compilation** → Macros expand to complete docstrings embedded in module
|
||||
3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract
|
||||
4. **Generation** → HTML/Markdown/Stub files created with transformed links
|
||||
5. **No drift**: Impossible for docs and code to disagree - they're the same file!
|
||||
|
||||
The macro system ensures complete, consistent documentation across all Python bindings.
|
||||
- Close issues automatically in gitea by adding to the commit message "closes #X", where X is the issue number. This associates the issue closure with the specific commit, so granular commits are preferred. You should only use the MCP tool to close issues directly when discovering that the issue is already complete; when committing changes, always such "closes" (or the opposite, "reopens") references to related issues. If on a feature branch, the issue will be referenced by the commit, and when merged to master, the issue will be actually closed (or reopened).
|
||||
|
|
@ -17,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
|
|
@ -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
|
||||
|
||||
|
|
|
|||
223
ROADMAP.md
|
|
@ -1,223 +0,0 @@
|
|||
# McRogueFace - Development Roadmap
|
||||
|
||||
## Project Status
|
||||
|
||||
**Current State**: Active development - C++ game engine with Python scripting
|
||||
**Latest Release**: Alpha 0.1
|
||||
**Issue Tracking**: See [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current tasks and bugs
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Strategic Vision
|
||||
|
||||
### Engine Philosophy
|
||||
|
||||
- **C++ First**: Performance-critical code stays in C++
|
||||
- **Python Close Behind**: Rich scripting without frame-rate impact
|
||||
- **Game-Ready**: Each improvement should benefit actual game development
|
||||
|
||||
### Architecture Goals
|
||||
|
||||
1. **Clean Inheritance**: Drawable → UI components, proper type preservation
|
||||
2. **Collection Consistency**: Uniform iteration, indexing, and search patterns
|
||||
3. **Resource Management**: RAII everywhere, proper lifecycle handling
|
||||
4. **Multi-Platform**: Windows/Linux feature parity maintained
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Decisions
|
||||
|
||||
### Three-Layer Grid Architecture
|
||||
Following successful roguelike patterns (Caves of Qud, Cogmind, DCSS):
|
||||
|
||||
1. **Visual Layer** (UIGridPoint) - Sprites, colors, animations
|
||||
2. **World State Layer** (TCODMap) - Walkability, transparency, physics
|
||||
3. **Entity Perspective Layer** (UIGridPointState) - Per-entity FOV, knowledge
|
||||
|
||||
### Performance Architecture
|
||||
Critical for large maps (1000x1000):
|
||||
|
||||
- **Spatial Hashing** for entity queries (not quadtrees!)
|
||||
- **Batch Operations** with context managers (10-100x speedup)
|
||||
- **Memory Pooling** for entities and components
|
||||
- **Dirty Flag System** to avoid unnecessary updates
|
||||
- **Zero-Copy NumPy Integration** via buffer protocol
|
||||
|
||||
### Key Insight from Research
|
||||
"Minimizing Python/C++ boundary crossings matters more than individual function complexity"
|
||||
- Batch everything possible
|
||||
- Use context managers for logical operations
|
||||
- Expose arrays, not individual cells
|
||||
- Profile and optimize hot paths only
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Development Phases
|
||||
|
||||
For detailed task tracking and current priorities, see the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).
|
||||
|
||||
### Phase 1: Foundation Stabilization ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #7 (Safe Constructors), #71 (Base Class), #87 (Visibility), #88 (Opacity)
|
||||
|
||||
### Phase 2: Constructor & API Polish ✅
|
||||
**Status**: Complete
|
||||
**Key Features**: Pythonic API, tuple support, standardized defaults
|
||||
|
||||
### Phase 3: Entity Lifecycle Management ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #30 (Entity.die()), #93 (Vector methods), #94 (Color helpers), #103 (Timer objects)
|
||||
|
||||
### Phase 4: Visibility & Performance ✅
|
||||
**Status**: Complete
|
||||
**Key Features**: AABB culling, name system, profiling tools
|
||||
|
||||
### Phase 5: Window/Scene Architecture ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #34 (Window object), #61 (Scene object), #1 (Resize events), #105 (Scene transitions)
|
||||
|
||||
### Phase 6: Rendering Revolution ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #50 (Grid backgrounds), #6 (RenderTexture), #8 (Viewport rendering)
|
||||
|
||||
### Phase 7: Documentation & Distribution ✅
|
||||
**Status**: Complete (2025-10-30)
|
||||
**Key Issues**: #85 (Docstrings), #86 (Parameter docs), #108 (Type stubs), #97 (API docs)
|
||||
**Completed**: All classes and functions converted to MCRF_* macro system with automated HTML/Markdown/man page generation
|
||||
|
||||
See [current open issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues?state=open) for active work.
|
||||
|
||||
---
|
||||
|
||||
## 🔮 Future Vision: Pure Python Extension Architecture
|
||||
|
||||
### Concept: McRogueFace as a Traditional Python Package
|
||||
**Status**: Long-term vision
|
||||
**Complexity**: Major architectural overhaul
|
||||
|
||||
Instead of being a C++ application that embeds Python, McRogueFace could be redesigned as a pure Python extension module that can be installed via `pip install mcrogueface`.
|
||||
|
||||
### Technical Approach
|
||||
|
||||
1. **Separate Core Engine from Python Embedding**
|
||||
- Extract SFML rendering, audio, and input into C++ extension modules
|
||||
- Remove embedded CPython interpreter
|
||||
- Use Python's C API to expose functionality
|
||||
|
||||
2. **Module Structure**
|
||||
```
|
||||
mcrfpy/
|
||||
├── __init__.py # Pure Python coordinator
|
||||
├── _core.so # C++ rendering/game loop extension
|
||||
├── _sfml.so # SFML bindings
|
||||
├── _audio.so # Audio system bindings
|
||||
└── engine.py # Python game engine logic
|
||||
```
|
||||
|
||||
3. **Inverted Control Flow**
|
||||
- Python drives the main loop instead of C++
|
||||
- C++ extensions handle performance-critical operations
|
||||
- Python manages game logic, scenes, and entity systems
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Standard Python Packaging**: `pip install mcrogueface`
|
||||
- **Virtual Environment Support**: Works with venv, conda, poetry
|
||||
- **Better IDE Integration**: Standard Python development workflow
|
||||
- **Easier Testing**: Use pytest, standard Python testing tools
|
||||
- **Cross-Python Compatibility**: Support multiple Python versions
|
||||
- **Modular Architecture**: Users can import only what they need
|
||||
|
||||
### Challenges
|
||||
|
||||
- **Major Refactoring**: Complete restructure of codebase
|
||||
- **Performance Considerations**: Python-driven main loop overhead
|
||||
- **Build Complexity**: Multiple extension modules to compile
|
||||
- **Platform Support**: Need wheels for many platform/Python combinations
|
||||
- **API Stability**: Would need careful design to maintain compatibility
|
||||
|
||||
### Example Usage (Future Vision)
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
from mcrfpy import Scene, Frame, Sprite, Grid
|
||||
|
||||
# Create game directly in Python
|
||||
game = mcrfpy.Game(width=1024, height=768)
|
||||
|
||||
# Define scenes using Python classes
|
||||
class MainMenu(Scene):
|
||||
def on_enter(self):
|
||||
self.ui.append(Frame(100, 100, 200, 50))
|
||||
self.ui.append(Sprite("logo.png", x=400, y=100))
|
||||
|
||||
def on_keypress(self, key, pressed):
|
||||
if key == "ENTER" and pressed:
|
||||
self.game.set_scene("game")
|
||||
|
||||
# Run the game
|
||||
game.add_scene("menu", MainMenu())
|
||||
game.run()
|
||||
```
|
||||
|
||||
This architecture would make McRogueFace a first-class Python citizen, following standard Python packaging conventions while maintaining high performance through C++ extensions.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Major Feature Areas
|
||||
|
||||
For current status and detailed tasks, see the corresponding Gitea issue labels:
|
||||
|
||||
### Core Systems
|
||||
- **UI/Rendering System**: Issues tagged `[Major Feature]` related to rendering
|
||||
- **Grid/Entity System**: Pathfinding, FOV, entity management
|
||||
- **Animation System**: Property animation, easing functions, callbacks
|
||||
- **Scene/Window Management**: Scene lifecycle, transitions, viewport
|
||||
|
||||
### Performance Optimization
|
||||
- **#115**: SpatialHash for 10,000+ entities
|
||||
- **#116**: Dirty flag system
|
||||
- **#113**: Batch operations for NumPy-style access
|
||||
- **#117**: Memory pool for entities
|
||||
|
||||
### Advanced Features
|
||||
- **#118**: Scene as Drawable (scenes can be drawn/animated)
|
||||
- **#122**: Parent-Child UI System
|
||||
- **#123**: Grid Subgrid System (256x256 chunks)
|
||||
- **#124**: Grid Point Animation
|
||||
- **#106**: Shader support
|
||||
- **#107**: Particle system
|
||||
|
||||
### Documentation
|
||||
- **#92**: Inline C++ documentation system
|
||||
- **#91**: Python type stub files (.pyi)
|
||||
- **#97**: Automated API documentation extraction
|
||||
- **#126**: Generate perfectly consistent Python interface
|
||||
|
||||
---
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
- **Issue Tracker**: [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues)
|
||||
- **Source Code**: [Gitea Repository](https://gamedev.ffwf.net/gitea/john/McRogueFace)
|
||||
- **Documentation**: See `CLAUDE.md` for build instructions and development guide
|
||||
- **Tutorial**: See `roguelike_tutorial/` for implementation examples
|
||||
- **Workflow**: See "Gitea-First Workflow" section in `CLAUDE.md` for issue management best practices
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Development Workflow
|
||||
|
||||
**Gitea is the Single Source of Truth** for this project. Before starting any work:
|
||||
|
||||
1. **Check Gitea Issues** for existing tasks, bugs, or related work
|
||||
2. **Create granular issues** for new features or problems
|
||||
3. **Update issues** when work affects other systems
|
||||
4. **Document discoveries** - if something is undocumented or misleading, create a task to fix it
|
||||
5. **Cross-reference commits** with issue numbers (e.g., "Fixes #104")
|
||||
|
||||
See the "Gitea-First Workflow" section in `CLAUDE.md` for detailed guidelines on efficient development practices using the Gitea MCP tools.
|
||||
|
||||
---
|
||||
|
||||
*For current priorities, task tracking, and bug reports, please use the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).*
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
@ -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!")
|
||||
|
|
@ -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")
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
@echo off
|
||||
REM Windows build script for McRogueFace
|
||||
REM Run this over SSH without Visual Studio GUI
|
||||
|
||||
echo Building McRogueFace for Windows...
|
||||
|
||||
REM Clean previous build
|
||||
if exist build_win rmdir /s /q build_win
|
||||
mkdir build_win
|
||||
cd build_win
|
||||
|
||||
REM Generate Visual Studio project files with CMake
|
||||
REM Use -G to specify generator, -A for architecture
|
||||
REM Visual Studio 2022 = "Visual Studio 17 2022"
|
||||
REM Visual Studio 2019 = "Visual Studio 16 2019"
|
||||
cmake -G "Visual Studio 17 2022" -A x64 ..
|
||||
if errorlevel 1 (
|
||||
echo CMake configuration failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build using MSBuild (comes with Visual Studio)
|
||||
REM You can also use cmake --build . --config Release
|
||||
msbuild McRogueFace.sln /p:Configuration=Release /p:Platform=x64 /m
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Build completed successfully!
|
||||
echo Executable location: build_win\Release\mcrogueface.exe
|
||||
|
||||
REM Alternative: Using cmake to build (works with any generator)
|
||||
REM cmake --build . --config Release --parallel
|
||||
|
||||
cd ..
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
@echo off
|
||||
REM Windows build script using cmake --build (generator-agnostic)
|
||||
REM This version works with any CMake generator
|
||||
|
||||
echo Building McRogueFace for Windows using CMake...
|
||||
|
||||
REM Set build directory
|
||||
set BUILD_DIR=build_win
|
||||
set CONFIG=Release
|
||||
|
||||
REM Clean previous build
|
||||
if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR%
|
||||
mkdir %BUILD_DIR%
|
||||
cd %BUILD_DIR%
|
||||
|
||||
REM Configure with CMake
|
||||
REM You can change the generator here if needed:
|
||||
REM -G "Visual Studio 17 2022" (VS 2022)
|
||||
REM -G "Visual Studio 16 2019" (VS 2019)
|
||||
REM -G "MinGW Makefiles" (MinGW)
|
||||
REM -G "Ninja" (Ninja build system)
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%CONFIG% ..
|
||||
if errorlevel 1 (
|
||||
echo CMake configuration failed!
|
||||
cd ..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build using cmake (works with any generator)
|
||||
cmake --build . --config %CONFIG% --parallel
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
cd ..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Build completed successfully!
|
||||
echo Executable: %BUILD_DIR%\%CONFIG%\mcrogueface.exe
|
||||
echo.
|
||||
|
||||
cd ..
|
||||
|
|
@ -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}"
|
||||
|
|
@ -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
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
1070
docs/mcrfpy.3
|
|
@ -1,532 +0,0 @@
|
|||
"""Type stubs for McRogueFace Python API.
|
||||
|
||||
Core game engine interface for creating roguelike games with Python.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
|
||||
|
||||
# Type aliases
|
||||
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid']
|
||||
Transition = Union[str, None]
|
||||
|
||||
# Classes
|
||||
|
||||
class Color:
|
||||
"""SFML Color Object for RGBA colors."""
|
||||
|
||||
r: int
|
||||
g: int
|
||||
b: int
|
||||
a: int
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
|
||||
|
||||
def from_hex(self, hex_string: str) -> 'Color':
|
||||
"""Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
|
||||
...
|
||||
|
||||
def to_hex(self) -> str:
|
||||
"""Convert color to hex string format."""
|
||||
...
|
||||
|
||||
def lerp(self, other: 'Color', t: float) -> 'Color':
|
||||
"""Linear interpolation between two colors."""
|
||||
...
|
||||
|
||||
class Vector:
|
||||
"""SFML Vector Object for 2D coordinates."""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float, y: float) -> None: ...
|
||||
|
||||
def add(self, other: 'Vector') -> 'Vector': ...
|
||||
def subtract(self, other: 'Vector') -> 'Vector': ...
|
||||
def multiply(self, scalar: float) -> 'Vector': ...
|
||||
def divide(self, scalar: float) -> 'Vector': ...
|
||||
def distance(self, other: 'Vector') -> float: ...
|
||||
def normalize(self) -> 'Vector': ...
|
||||
def dot(self, other: 'Vector') -> float: ...
|
||||
|
||||
class Texture:
|
||||
"""SFML Texture Object for images."""
|
||||
|
||||
def __init__(self, filename: str) -> None: ...
|
||||
|
||||
filename: str
|
||||
width: int
|
||||
height: int
|
||||
sprite_count: int
|
||||
|
||||
class Font:
|
||||
"""SFML Font Object for text rendering."""
|
||||
|
||||
def __init__(self, filename: str) -> None: ...
|
||||
|
||||
filename: str
|
||||
family: str
|
||||
|
||||
class Drawable:
|
||||
"""Base class for all drawable UI elements."""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
visible: bool
|
||||
z_index: int
|
||||
name: str
|
||||
pos: Vector
|
||||
|
||||
def get_bounds(self) -> Tuple[float, float, float, float]:
|
||||
"""Get bounding box as (x, y, width, height)."""
|
||||
...
|
||||
|
||||
def move(self, dx: float, dy: float) -> None:
|
||||
"""Move by relative offset (dx, dy)."""
|
||||
...
|
||||
|
||||
def resize(self, width: float, height: float) -> None:
|
||||
"""Resize to new dimensions (width, height)."""
|
||||
...
|
||||
|
||||
class Frame(Drawable):
|
||||
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)
|
||||
|
||||
A rectangular frame UI element that can contain other drawable elements.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0,
|
||||
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
|
||||
outline: float = 0, click: Optional[Callable] = None,
|
||||
children: Optional[List[UIElement]] = None) -> None: ...
|
||||
|
||||
w: float
|
||||
h: float
|
||||
fill_color: Color
|
||||
outline_color: Color
|
||||
outline: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
children: 'UICollection'
|
||||
clip_children: bool
|
||||
|
||||
class Caption(Drawable):
|
||||
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)
|
||||
|
||||
A text display UI element with customizable font and styling.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, text: str = '', x: float = 0, y: float = 0,
|
||||
font: Optional[Font] = None, fill_color: Optional[Color] = None,
|
||||
outline_color: Optional[Color] = None, outline: float = 0,
|
||||
click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
text: str
|
||||
font: Font
|
||||
fill_color: Color
|
||||
outline_color: Color
|
||||
outline: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
w: float # Read-only, computed from text
|
||||
h: float # Read-only, computed from text
|
||||
|
||||
class Sprite(Drawable):
|
||||
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)
|
||||
|
||||
A sprite UI element that displays a texture or portion of a texture atlas.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None,
|
||||
sprite_index: int = 0, scale: float = 1.0,
|
||||
click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
texture: Texture
|
||||
sprite_index: int
|
||||
scale: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
w: float # Read-only, computed from texture
|
||||
h: float # Read-only, computed from texture
|
||||
|
||||
class Grid(Drawable):
|
||||
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)
|
||||
|
||||
A grid-based tilemap UI element for rendering tile-based levels and game worlds.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20),
|
||||
texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16,
|
||||
scale: float = 1.0, click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
grid_size: Tuple[int, int]
|
||||
tile_width: int
|
||||
tile_height: int
|
||||
texture: Texture
|
||||
scale: float
|
||||
points: List[List['GridPoint']]
|
||||
entities: 'EntityCollection'
|
||||
background_color: Color
|
||||
click: Optional[Callable[[int, int, int], None]]
|
||||
|
||||
def at(self, x: int, y: int) -> 'GridPoint':
|
||||
"""Get grid point at tile coordinates."""
|
||||
...
|
||||
|
||||
class GridPoint:
|
||||
"""Grid point representing a single tile."""
|
||||
|
||||
texture_index: int
|
||||
solid: bool
|
||||
color: Color
|
||||
|
||||
class GridPointState:
|
||||
"""State information for a grid point."""
|
||||
|
||||
texture_index: int
|
||||
color: Color
|
||||
|
||||
class Entity(Drawable):
|
||||
"""Entity(grid_x=0, grid_y=0, texture=None, sprite_index=0, name='')
|
||||
|
||||
Game entity that lives within a Grid.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, grid_x: float = 0, grid_y: float = 0, texture: Optional[Texture] = None,
|
||||
sprite_index: int = 0, name: str = '') -> None: ...
|
||||
|
||||
grid_x: float
|
||||
grid_y: float
|
||||
texture: Texture
|
||||
sprite_index: int
|
||||
grid: Optional[Grid]
|
||||
|
||||
def at(self, grid_x: float, grid_y: float) -> None:
|
||||
"""Move entity to grid position."""
|
||||
...
|
||||
|
||||
def die(self) -> None:
|
||||
"""Remove entity from its grid."""
|
||||
...
|
||||
|
||||
def index(self) -> int:
|
||||
"""Get index in parent grid's entity collection."""
|
||||
...
|
||||
|
||||
class UICollection:
|
||||
"""Collection of UI drawable elements (Frame, Caption, Sprite, Grid)."""
|
||||
|
||||
def __len__(self) -> int: ...
|
||||
def __getitem__(self, index: int) -> UIElement: ...
|
||||
def __setitem__(self, index: int, value: UIElement) -> None: ...
|
||||
def __delitem__(self, index: int) -> None: ...
|
||||
def __contains__(self, item: UIElement) -> bool: ...
|
||||
def __iter__(self) -> Any: ...
|
||||
def __add__(self, other: 'UICollection') -> 'UICollection': ...
|
||||
def __iadd__(self, other: 'UICollection') -> 'UICollection': ...
|
||||
|
||||
def append(self, item: UIElement) -> None: ...
|
||||
def extend(self, items: List[UIElement]) -> None: ...
|
||||
def remove(self, item: UIElement) -> None: ...
|
||||
def index(self, item: UIElement) -> int: ...
|
||||
def count(self, item: UIElement) -> int: ...
|
||||
|
||||
class EntityCollection:
|
||||
"""Collection of Entity objects."""
|
||||
|
||||
def __len__(self) -> int: ...
|
||||
def __getitem__(self, index: int) -> Entity: ...
|
||||
def __setitem__(self, index: int, value: Entity) -> None: ...
|
||||
def __delitem__(self, index: int) -> None: ...
|
||||
def __contains__(self, item: Entity) -> bool: ...
|
||||
def __iter__(self) -> Any: ...
|
||||
def __add__(self, other: 'EntityCollection') -> 'EntityCollection': ...
|
||||
def __iadd__(self, other: 'EntityCollection') -> 'EntityCollection': ...
|
||||
|
||||
def append(self, item: Entity) -> None: ...
|
||||
def extend(self, items: List[Entity]) -> None: ...
|
||||
def remove(self, item: Entity) -> None: ...
|
||||
def index(self, item: Entity) -> int: ...
|
||||
def count(self, item: Entity) -> int: ...
|
||||
|
||||
class Scene:
|
||||
"""Base class for object-oriented scenes."""
|
||||
|
||||
name: str
|
||||
|
||||
def __init__(self, name: str) -> None: ...
|
||||
|
||||
def activate(self) -> None:
|
||||
"""Called when scene becomes active."""
|
||||
...
|
||||
|
||||
def deactivate(self) -> None:
|
||||
"""Called when scene becomes inactive."""
|
||||
...
|
||||
|
||||
def get_ui(self) -> UICollection:
|
||||
"""Get UI elements collection."""
|
||||
...
|
||||
|
||||
def on_keypress(self, key: str, pressed: bool) -> None:
|
||||
"""Handle keyboard events."""
|
||||
...
|
||||
|
||||
def on_click(self, x: float, y: float, button: int) -> None:
|
||||
"""Handle mouse clicks."""
|
||||
...
|
||||
|
||||
def on_enter(self) -> None:
|
||||
"""Called when entering the scene."""
|
||||
...
|
||||
|
||||
def on_exit(self) -> None:
|
||||
"""Called when leaving the scene."""
|
||||
...
|
||||
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
"""Handle window resize events."""
|
||||
...
|
||||
|
||||
def update(self, dt: float) -> None:
|
||||
"""Update scene logic."""
|
||||
...
|
||||
|
||||
class Timer:
|
||||
"""Timer object for scheduled callbacks."""
|
||||
|
||||
name: str
|
||||
interval: int
|
||||
active: bool
|
||||
|
||||
def __init__(self, name: str, callback: Callable[[float], None], interval: int) -> None: ...
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause the timer."""
|
||||
...
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Resume the timer."""
|
||||
...
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""Cancel and remove the timer."""
|
||||
...
|
||||
|
||||
class Window:
|
||||
"""Window singleton for managing the game window."""
|
||||
|
||||
resolution: Tuple[int, int]
|
||||
fullscreen: bool
|
||||
vsync: bool
|
||||
title: str
|
||||
fps_limit: int
|
||||
game_resolution: Tuple[int, int]
|
||||
scaling_mode: str
|
||||
|
||||
@staticmethod
|
||||
def get() -> 'Window':
|
||||
"""Get the window singleton instance."""
|
||||
...
|
||||
|
||||
class Animation:
|
||||
"""Animation object for animating UI properties."""
|
||||
|
||||
target: Any
|
||||
property: str
|
||||
duration: float
|
||||
easing: str
|
||||
loop: bool
|
||||
on_complete: Optional[Callable]
|
||||
|
||||
def __init__(self, target: Any, property: str, start_value: Any, end_value: Any,
|
||||
duration: float, easing: str = 'linear', loop: bool = False,
|
||||
on_complete: Optional[Callable] = None) -> None: ...
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the animation."""
|
||||
...
|
||||
|
||||
def update(self, dt: float) -> bool:
|
||||
"""Update animation, returns True if still running."""
|
||||
...
|
||||
|
||||
def get_current_value(self) -> Any:
|
||||
"""Get the current interpolated value."""
|
||||
...
|
||||
|
||||
# Module functions
|
||||
|
||||
def createSoundBuffer(filename: str) -> int:
|
||||
"""Load a sound effect from a file and return its buffer ID."""
|
||||
...
|
||||
|
||||
def loadMusic(filename: str) -> None:
|
||||
"""Load and immediately play background music from a file."""
|
||||
...
|
||||
|
||||
def setMusicVolume(volume: int) -> None:
|
||||
"""Set the global music volume (0-100)."""
|
||||
...
|
||||
|
||||
def setSoundVolume(volume: int) -> None:
|
||||
"""Set the global sound effects volume (0-100)."""
|
||||
...
|
||||
|
||||
def playSound(buffer_id: int) -> None:
|
||||
"""Play a sound effect using a previously loaded buffer."""
|
||||
...
|
||||
|
||||
def getMusicVolume() -> int:
|
||||
"""Get the current music volume level (0-100)."""
|
||||
...
|
||||
|
||||
def getSoundVolume() -> int:
|
||||
"""Get the current sound effects volume level (0-100)."""
|
||||
...
|
||||
|
||||
def sceneUI(scene: Optional[str] = None) -> UICollection:
|
||||
"""Get all UI elements for a scene."""
|
||||
...
|
||||
|
||||
def currentScene() -> str:
|
||||
"""Get the name of the currently active scene."""
|
||||
...
|
||||
|
||||
def setScene(scene: str, transition: Optional[str] = None, duration: float = 0.0) -> None:
|
||||
"""Switch to a different scene with optional transition effect."""
|
||||
...
|
||||
|
||||
def createScene(name: str) -> None:
|
||||
"""Create a new empty scene."""
|
||||
...
|
||||
|
||||
def keypressScene(handler: Callable[[str, bool], None]) -> None:
|
||||
"""Set the keyboard event handler for the current scene."""
|
||||
...
|
||||
|
||||
def setTimer(name: str, handler: Callable[[float], None], interval: int) -> None:
|
||||
"""Create or update a recurring timer."""
|
||||
...
|
||||
|
||||
def delTimer(name: str) -> None:
|
||||
"""Stop and remove a timer."""
|
||||
...
|
||||
|
||||
def exit() -> None:
|
||||
"""Cleanly shut down the game engine and exit the application."""
|
||||
...
|
||||
|
||||
def setScale(multiplier: float) -> None:
|
||||
"""Scale the game window size (deprecated - use Window.resolution)."""
|
||||
...
|
||||
|
||||
def find(name: str, scene: Optional[str] = None) -> Optional[UIElement]:
|
||||
"""Find the first UI element with the specified name."""
|
||||
...
|
||||
|
||||
def findAll(pattern: str, scene: Optional[str] = None) -> List[UIElement]:
|
||||
"""Find all UI elements matching a name pattern (supports * wildcards)."""
|
||||
...
|
||||
|
||||
def getMetrics() -> Dict[str, Union[int, float]]:
|
||||
"""Get current performance metrics."""
|
||||
...
|
||||
|
||||
# Submodule
|
||||
class automation:
|
||||
"""Automation API for testing and scripting."""
|
||||
|
||||
@staticmethod
|
||||
def screenshot(filename: str) -> bool:
|
||||
"""Save a screenshot to the specified file."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def position() -> Tuple[int, int]:
|
||||
"""Get current mouse position as (x, y) tuple."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def size() -> Tuple[int, int]:
|
||||
"""Get screen size as (width, height) tuple."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def onScreen(x: int, y: int) -> bool:
|
||||
"""Check if coordinates are within screen bounds."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def moveTo(x: int, y: int, duration: float = 0.0) -> None:
|
||||
"""Move mouse to absolute position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def moveRel(xOffset: int, yOffset: int, duration: float = 0.0) -> None:
|
||||
"""Move mouse relative to current position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def dragTo(x: int, y: int, duration: float = 0.0, button: str = 'left') -> None:
|
||||
"""Drag mouse to position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def dragRel(xOffset: int, yOffset: int, duration: float = 0.0, button: str = 'left') -> None:
|
||||
"""Drag mouse relative to current position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def click(x: Optional[int] = None, y: Optional[int] = None, clicks: int = 1,
|
||||
interval: float = 0.0, button: str = 'left') -> None:
|
||||
"""Click mouse at position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def mouseDown(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
|
||||
"""Press mouse button down."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def mouseUp(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
|
||||
"""Release mouse button."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def keyDown(key: str) -> None:
|
||||
"""Press key down."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def keyUp(key: str) -> None:
|
||||
"""Release key."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def press(key: str) -> None:
|
||||
"""Press and release a key."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def typewrite(text: str, interval: float = 0.0) -> None:
|
||||
"""Type text with optional interval between characters."""
|
||||
...
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
"""Type stubs for McRogueFace Python API.
|
||||
|
||||
Auto-generated - do not edit directly.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Dict, Tuple, Optional, Callable, Union
|
||||
|
||||
# Module documentation
|
||||
# McRogueFace Python API\n\nCore game engine interface for creating roguelike games with Python.\n\nThis module provides:\n- Scene management (createScene, setScene, currentScene)\n- UI components (Frame, Caption, Sprite, Grid)\n- Entity system for game objects\n- Audio playback (sound effects and music)\n- Timer system for scheduled events\n- Input handling\n- Performance metrics\n\nExample:\n import mcrfpy\n \n # Create a new scene\n mcrfpy.createScene('game')\n mcrfpy.setScene('game')\n \n # Add UI elements\n frame = mcrfpy.Frame(10, 10, 200, 100)\n caption = mcrfpy.Caption('Hello World', 50, 50)\n mcrfpy.sceneUI().extend([frame, caption])\n
|
||||
|
||||
# Classes
|
||||
|
||||
class Animation:
|
||||
"""Animation object for animating UI properties"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_current_value(self, *args, **kwargs) -> Any: ...
|
||||
def start(self, *args, **kwargs) -> Any: ...
|
||||
def update(selfreturns True if still running) -> Any: ...
|
||||
|
||||
class Caption:
|
||||
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Color:
|
||||
"""SFML Color Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def from_hex(selfe.g., '#FF0000' or 'FF0000') -> Any: ...
|
||||
def lerp(self, *args, **kwargs) -> Any: ...
|
||||
def to_hex(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Drawable:
|
||||
"""Base class for all drawable UI elements"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Entity:
|
||||
"""UIEntity objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def at(self, *args, **kwargs) -> Any: ...
|
||||
def die(self, *args, **kwargs) -> Any: ...
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def path_to(selfx: int, y: int) -> bool: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
def update_visibility(self) -> None: ...
|
||||
|
||||
class EntityCollection:
|
||||
"""Iterable, indexable collection of Entities"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def append(self, *args, **kwargs) -> Any: ...
|
||||
def count(self, *args, **kwargs) -> Any: ...
|
||||
def extend(self, *args, **kwargs) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def remove(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Font:
|
||||
"""SFML Font Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Frame:
|
||||
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Grid:
|
||||
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def at(self, *args, **kwargs) -> Any: ...
|
||||
def compute_astar_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
|
||||
def compute_dijkstra(selfroot_x: int, root_y: int, diagonal_cost: float = 1.41) -> None: ...
|
||||
def compute_fov(selfx: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None: ...
|
||||
def find_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def get_dijkstra_distance(selfx: int, y: int) -> Optional[float]: ...
|
||||
def get_dijkstra_path(selfx: int, y: int) -> List[Tuple[int, int]]: ...
|
||||
def is_in_fov(selfx: int, y: int) -> bool: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class GridPoint:
|
||||
"""UIGridPoint object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class GridPointState:
|
||||
"""UIGridPointState object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Scene:
|
||||
"""Base class for object-oriented scenes"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def activate(self, *args, **kwargs) -> Any: ...
|
||||
def get_ui(self, *args, **kwargs) -> Any: ...
|
||||
def register_keyboard(selfalternative to overriding on_keypress) -> Any: ...
|
||||
|
||||
class Sprite:
|
||||
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Texture:
|
||||
"""SFML Texture Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Timer:
|
||||
"""Timer object for scheduled callbacks"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def cancel(self, *args, **kwargs) -> Any: ...
|
||||
def pause(self, *args, **kwargs) -> Any: ...
|
||||
def restart(self, *args, **kwargs) -> Any: ...
|
||||
def resume(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class UICollection:
|
||||
"""Iterable, indexable collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def append(self, *args, **kwargs) -> Any: ...
|
||||
def count(self, *args, **kwargs) -> Any: ...
|
||||
def extend(self, *args, **kwargs) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def remove(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class UICollectionIter:
|
||||
"""Iterator for a collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class UIEntityCollectionIter:
|
||||
"""Iterator for a collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Vector:
|
||||
"""SFML Vector Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def angle(self, *args, **kwargs) -> Any: ...
|
||||
def copy(self, *args, **kwargs) -> Any: ...
|
||||
def distance_to(self, *args, **kwargs) -> Any: ...
|
||||
def dot(self, *args, **kwargs) -> Any: ...
|
||||
def magnitude(self, *args, **kwargs) -> Any: ...
|
||||
def magnitude_squared(self, *args, **kwargs) -> Any: ...
|
||||
def normalize(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Window:
|
||||
"""Window singleton for accessing and modifying the game window properties"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def center(self, *args, **kwargs) -> Any: ...
|
||||
def get(self, *args, **kwargs) -> Any: ...
|
||||
def screenshot(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
# Functions
|
||||
|
||||
def createScene(name: str) -> None: ...
|
||||
def createSoundBuffer(filename: str) -> int: ...
|
||||
def currentScene() -> str: ...
|
||||
def delTimer(name: str) -> None: ...
|
||||
def exit() -> None: ...
|
||||
def find(name: str, scene: str = None) -> UIDrawable | None: ...
|
||||
def findAll(pattern: str, scene: str = None) -> list: ...
|
||||
def getMetrics() -> dict: ...
|
||||
def getMusicVolume() -> int: ...
|
||||
def getSoundVolume() -> int: ...
|
||||
def keypressScene(handler: callable) -> None: ...
|
||||
def loadMusic(filename: str) -> None: ...
|
||||
def playSound(buffer_id: int) -> None: ...
|
||||
def sceneUI(scene: str = None) -> list: ...
|
||||
def setMusicVolume(volume: int) -> None: ...
|
||||
def setScale(multiplier: float) -> None: ...
|
||||
def setScene(scene: str, transition: str = None, duration: float = 0.0) -> None: ...
|
||||
def setSoundVolume(volume: int) -> None: ...
|
||||
def setTimer(name: str, handler: callable, interval: int) -> None: ...
|
||||
|
||||
# Constants
|
||||
|
||||
FOV_BASIC: int
|
||||
FOV_DIAMOND: int
|
||||
FOV_PERMISSIVE_0: int
|
||||
FOV_PERMISSIVE_1: int
|
||||
FOV_PERMISSIVE_2: int
|
||||
FOV_PERMISSIVE_3: int
|
||||
FOV_PERMISSIVE_4: int
|
||||
FOV_PERMISSIVE_5: int
|
||||
FOV_PERMISSIVE_6: int
|
||||
FOV_PERMISSIVE_7: int
|
||||
FOV_PERMISSIVE_8: int
|
||||
FOV_RESTRICTIVE: int
|
||||
FOV_SHADOW: int
|
||||
default_font: Any
|
||||
default_texture: Any
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
"""Type stubs for McRogueFace automation API."""
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
def click(x=None, y=None, clicks=1, interval=0.0, button='left') -> Any: ...
|
||||
def doubleClick(x=None, y=None) -> Any: ...
|
||||
def dragRel(xOffset, yOffset, duration=0.0, button='left') -> Any: ...
|
||||
def dragTo(x, y, duration=0.0, button='left') -> Any: ...
|
||||
def hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c')) -> Any: ...
|
||||
def keyDown(key) -> Any: ...
|
||||
def keyUp(key) -> Any: ...
|
||||
def middleClick(x=None, y=None) -> Any: ...
|
||||
def mouseDown(x=None, y=None, button='left') -> Any: ...
|
||||
def mouseUp(x=None, y=None, button='left') -> Any: ...
|
||||
def moveRel(xOffset, yOffset, duration=0.0) -> Any: ...
|
||||
def moveTo(x, y, duration=0.0) -> Any: ...
|
||||
def onScreen(x, y) -> Any: ...
|
||||
def position() - Get current mouse position as (x, y) -> Any: ...
|
||||
def rightClick(x=None, y=None) -> Any: ...
|
||||
def screenshot(filename) -> Any: ...
|
||||
def scroll(clicks, x=None, y=None) -> Any: ...
|
||||
def size() - Get screen size as (width, height) -> Any: ...
|
||||
def tripleClick(x=None, y=None) -> Any: ...
|
||||
def typewrite(message, interval=0.0) -> Any: ...
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
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
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 34ae258a863c4f6446effee28ca8ecae51b1519f
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 3b4b65dc9aae7d21a98d3578e3e9433728b118bb
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
#include "BenchmarkLogger.h"
|
||||
#include "GameEngine.h"
|
||||
|
||||
// Global benchmark logger instance
|
||||
BenchmarkLogger g_benchmarkLogger;
|
||||
|
||||
void BenchmarkLogger::recordFrame(const ProfilingMetrics& metrics) {
|
||||
if (!running) return;
|
||||
|
||||
auto now = std::chrono::high_resolution_clock::now();
|
||||
double timestamp_ms = std::chrono::duration<double, std::milli>(now - start_time).count();
|
||||
|
||||
BenchmarkFrame frame;
|
||||
frame.frame_number = ++frame_counter;
|
||||
frame.timestamp_ms = timestamp_ms;
|
||||
frame.frame_time_ms = metrics.frameTime;
|
||||
frame.fps = metrics.fps;
|
||||
|
||||
frame.work_time_ms = metrics.workTime;
|
||||
frame.grid_render_ms = metrics.gridRenderTime;
|
||||
frame.entity_render_ms = metrics.entityRenderTime;
|
||||
frame.python_time_ms = metrics.pythonScriptTime;
|
||||
frame.animation_time_ms = metrics.animationTime;
|
||||
frame.fov_overlay_ms = metrics.fovOverlayTime;
|
||||
|
||||
frame.draw_calls = metrics.drawCalls;
|
||||
frame.ui_elements = metrics.uiElements;
|
||||
frame.visible_elements = metrics.visibleElements;
|
||||
frame.grid_cells_rendered = metrics.gridCellsRendered;
|
||||
frame.entities_rendered = metrics.entitiesRendered;
|
||||
frame.total_entities = metrics.totalEntities;
|
||||
|
||||
// Move pending logs to this frame
|
||||
frame.logs = std::move(pending_logs);
|
||||
pending_logs.clear();
|
||||
|
||||
frames.push_back(std::move(frame));
|
||||
}
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <stdexcept>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <process.h>
|
||||
#define getpid _getpid
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// Forward declaration
|
||||
struct ProfilingMetrics;
|
||||
|
||||
/**
|
||||
* @brief Frame data captured during benchmarking
|
||||
*/
|
||||
struct BenchmarkFrame {
|
||||
int frame_number;
|
||||
double timestamp_ms; // Time since benchmark start
|
||||
float frame_time_ms;
|
||||
int fps;
|
||||
|
||||
// Detailed timing breakdown
|
||||
float work_time_ms; // Actual work time (frame_time - sleep_time)
|
||||
float grid_render_ms;
|
||||
float entity_render_ms;
|
||||
float python_time_ms;
|
||||
float animation_time_ms;
|
||||
float fov_overlay_ms;
|
||||
|
||||
// Counts
|
||||
int draw_calls;
|
||||
int ui_elements;
|
||||
int visible_elements;
|
||||
int grid_cells_rendered;
|
||||
int entities_rendered;
|
||||
int total_entities;
|
||||
|
||||
// User-provided log messages for this frame
|
||||
std::vector<std::string> logs;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Benchmark logging system for capturing performance data to JSON files
|
||||
*
|
||||
* Usage from Python:
|
||||
* mcrfpy.start_benchmark() # Start capturing
|
||||
* mcrfpy.log_benchmark("msg") # Add comment to current frame
|
||||
* filename = mcrfpy.end_benchmark() # Stop and get filename
|
||||
*/
|
||||
class BenchmarkLogger {
|
||||
private:
|
||||
bool running;
|
||||
std::string filename;
|
||||
std::chrono::high_resolution_clock::time_point start_time;
|
||||
std::vector<BenchmarkFrame> frames;
|
||||
std::vector<std::string> pending_logs; // Logs for current frame (before it's recorded)
|
||||
int frame_counter;
|
||||
|
||||
// Generate filename based on PID and timestamp
|
||||
std::string generateFilename() {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm tm = *std::localtime(&time_t);
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "benchmark_" << getpid() << "_"
|
||||
<< std::put_time(&tm, "%Y%m%d_%H%M%S") << ".json";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Get current timestamp as ISO 8601 string
|
||||
std::string getCurrentTimestamp() {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm tm = *std::localtime(&time_t);
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S");
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Escape string for JSON
|
||||
std::string escapeJson(const std::string& str) {
|
||||
std::ostringstream oss;
|
||||
for (char c : str) {
|
||||
switch (c) {
|
||||
case '"': oss << "\\\""; break;
|
||||
case '\\': oss << "\\\\"; break;
|
||||
case '\b': oss << "\\b"; break;
|
||||
case '\f': oss << "\\f"; break;
|
||||
case '\n': oss << "\\n"; break;
|
||||
case '\r': oss << "\\r"; break;
|
||||
case '\t': oss << "\\t"; break;
|
||||
default:
|
||||
if ('\x00' <= c && c <= '\x1f') {
|
||||
oss << "\\u" << std::hex << std::setw(4) << std::setfill('0') << (int)c;
|
||||
} else {
|
||||
oss << c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string start_timestamp;
|
||||
|
||||
public:
|
||||
BenchmarkLogger() : running(false), frame_counter(0) {}
|
||||
|
||||
/**
|
||||
* @brief Start benchmark logging
|
||||
* @throws std::runtime_error if already running
|
||||
*/
|
||||
void start() {
|
||||
if (running) {
|
||||
throw std::runtime_error("Benchmark already running. Call end_benchmark() first.");
|
||||
}
|
||||
|
||||
running = true;
|
||||
filename = generateFilename();
|
||||
start_time = std::chrono::high_resolution_clock::now();
|
||||
start_timestamp = getCurrentTimestamp();
|
||||
frames.clear();
|
||||
pending_logs.clear();
|
||||
frame_counter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop benchmark logging and write to file
|
||||
* @return The filename that was written
|
||||
* @throws std::runtime_error if not running
|
||||
*/
|
||||
std::string end() {
|
||||
if (!running) {
|
||||
throw std::runtime_error("No benchmark running. Call start_benchmark() first.");
|
||||
}
|
||||
|
||||
running = false;
|
||||
|
||||
// Calculate duration
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
double duration_seconds = std::chrono::duration<double>(end_time - start_time).count();
|
||||
std::string end_timestamp = getCurrentTimestamp();
|
||||
|
||||
// Write JSON file
|
||||
std::ofstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Failed to open benchmark file for writing: " + filename);
|
||||
}
|
||||
|
||||
file << "{\n";
|
||||
file << " \"benchmark\": {\n";
|
||||
file << " \"pid\": " << getpid() << ",\n";
|
||||
file << " \"start_time\": \"" << start_timestamp << "\",\n";
|
||||
file << " \"end_time\": \"" << end_timestamp << "\",\n";
|
||||
file << " \"total_frames\": " << frames.size() << ",\n";
|
||||
file << " \"duration_seconds\": " << std::fixed << std::setprecision(3) << duration_seconds << "\n";
|
||||
file << " },\n";
|
||||
|
||||
file << " \"frames\": [\n";
|
||||
for (size_t i = 0; i < frames.size(); ++i) {
|
||||
const auto& f = frames[i];
|
||||
file << " {\n";
|
||||
file << " \"frame_number\": " << f.frame_number << ",\n";
|
||||
file << " \"timestamp_ms\": " << std::fixed << std::setprecision(3) << f.timestamp_ms << ",\n";
|
||||
file << " \"frame_time_ms\": " << std::setprecision(3) << f.frame_time_ms << ",\n";
|
||||
file << " \"fps\": " << f.fps << ",\n";
|
||||
file << " \"work_time_ms\": " << std::setprecision(3) << f.work_time_ms << ",\n";
|
||||
file << " \"grid_render_ms\": " << std::setprecision(3) << f.grid_render_ms << ",\n";
|
||||
file << " \"entity_render_ms\": " << std::setprecision(3) << f.entity_render_ms << ",\n";
|
||||
file << " \"python_time_ms\": " << std::setprecision(3) << f.python_time_ms << ",\n";
|
||||
file << " \"animation_time_ms\": " << std::setprecision(3) << f.animation_time_ms << ",\n";
|
||||
file << " \"fov_overlay_ms\": " << std::setprecision(3) << f.fov_overlay_ms << ",\n";
|
||||
file << " \"draw_calls\": " << f.draw_calls << ",\n";
|
||||
file << " \"ui_elements\": " << f.ui_elements << ",\n";
|
||||
file << " \"visible_elements\": " << f.visible_elements << ",\n";
|
||||
file << " \"grid_cells_rendered\": " << f.grid_cells_rendered << ",\n";
|
||||
file << " \"entities_rendered\": " << f.entities_rendered << ",\n";
|
||||
file << " \"total_entities\": " << f.total_entities << ",\n";
|
||||
|
||||
// Write logs array
|
||||
file << " \"logs\": [";
|
||||
for (size_t j = 0; j < f.logs.size(); ++j) {
|
||||
file << "\"" << escapeJson(f.logs[j]) << "\"";
|
||||
if (j < f.logs.size() - 1) file << ", ";
|
||||
}
|
||||
file << "]\n";
|
||||
|
||||
file << " }";
|
||||
if (i < frames.size() - 1) file << ",";
|
||||
file << "\n";
|
||||
}
|
||||
file << " ]\n";
|
||||
file << "}\n";
|
||||
|
||||
file.close();
|
||||
|
||||
std::string result = filename;
|
||||
filename.clear();
|
||||
frames.clear();
|
||||
pending_logs.clear();
|
||||
frame_counter = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add a log message to the current frame
|
||||
* @param message The message to log
|
||||
* @throws std::runtime_error if not running
|
||||
*/
|
||||
void log(const std::string& message) {
|
||||
if (!running) {
|
||||
throw std::runtime_error("No benchmark running. Call start_benchmark() first.");
|
||||
}
|
||||
pending_logs.push_back(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Record frame data (called by game loop at end of each frame)
|
||||
* @param metrics The current frame's profiling metrics
|
||||
*/
|
||||
void recordFrame(const ProfilingMetrics& metrics);
|
||||
|
||||
/**
|
||||
* @brief Check if benchmark is currently running
|
||||
*/
|
||||
bool isRunning() const { return running; }
|
||||
|
||||
/**
|
||||
* @brief Get current frame count
|
||||
*/
|
||||
int getFrameCount() const { return frame_counter; }
|
||||
};
|
||||
|
||||
// Global benchmark logger instance
|
||||
extern BenchmarkLogger g_benchmarkLogger;
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
164
src/GameEngine.h
|
|
@ -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);
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,201 +0,0 @@
|
|||
#include "GridChunk.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyTexture.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// =============================================================================
|
||||
// GridChunk implementation
|
||||
// =============================================================================
|
||||
|
||||
GridChunk::GridChunk(int chunk_x, int chunk_y, int width, int height,
|
||||
int world_x, int world_y, UIGrid* parent)
|
||||
: chunk_x(chunk_x), chunk_y(chunk_y),
|
||||
width(width), height(height),
|
||||
world_x(world_x), world_y(world_y),
|
||||
cells(width * height),
|
||||
dirty(true),
|
||||
parent_grid(parent)
|
||||
{}
|
||||
|
||||
UIGridPoint& GridChunk::at(int local_x, int local_y) {
|
||||
return cells[local_y * width + local_x];
|
||||
}
|
||||
|
||||
const UIGridPoint& GridChunk::at(int local_x, int local_y) const {
|
||||
return cells[local_y * width + local_x];
|
||||
}
|
||||
|
||||
void GridChunk::markDirty() {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// #150 - Removed ensureTexture/renderToTexture - base layer rendering removed
|
||||
// GridChunk now only provides data storage for GridPoints
|
||||
|
||||
sf::FloatRect GridChunk::getWorldBounds(int cell_width, int cell_height) const {
|
||||
return sf::FloatRect(
|
||||
sf::Vector2f(world_x * cell_width, world_y * cell_height),
|
||||
sf::Vector2f(width * cell_width, height * cell_height)
|
||||
);
|
||||
}
|
||||
|
||||
bool GridChunk::isVisible(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge) const {
|
||||
// Check if chunk's cell range overlaps with viewport's cell range
|
||||
float chunk_right = world_x + width;
|
||||
float chunk_bottom = world_y + height;
|
||||
|
||||
return !(world_x >= right_edge || chunk_right <= left_edge ||
|
||||
world_y >= bottom_edge || chunk_bottom <= top_edge);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ChunkManager implementation
|
||||
// =============================================================================
|
||||
|
||||
ChunkManager::ChunkManager(int grid_x, int grid_y, UIGrid* parent)
|
||||
: grid_x(grid_x), grid_y(grid_y), parent_grid(parent)
|
||||
{
|
||||
// Calculate number of chunks needed
|
||||
chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
|
||||
chunks.reserve(chunks_x * chunks_y);
|
||||
|
||||
// Create chunks
|
||||
for (int cy = 0; cy < chunks_y; ++cy) {
|
||||
for (int cx = 0; cx < chunks_x; ++cx) {
|
||||
// Calculate world position
|
||||
int world_x = cx * GridChunk::CHUNK_SIZE;
|
||||
int world_y = cy * GridChunk::CHUNK_SIZE;
|
||||
|
||||
// Calculate actual size (may be smaller at edges)
|
||||
int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x);
|
||||
int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y);
|
||||
|
||||
chunks.push_back(std::make_unique<GridChunk>(
|
||||
cx, cy, chunk_width, chunk_height, world_x, world_y, parent
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridChunk* ChunkManager::getChunkForCell(int x, int y) {
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int chunk_x = x / GridChunk::CHUNK_SIZE;
|
||||
int chunk_y = y / GridChunk::CHUNK_SIZE;
|
||||
return getChunk(chunk_x, chunk_y);
|
||||
}
|
||||
|
||||
const GridChunk* ChunkManager::getChunkForCell(int x, int y) const {
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int chunk_x = x / GridChunk::CHUNK_SIZE;
|
||||
int chunk_y = y / GridChunk::CHUNK_SIZE;
|
||||
return getChunk(chunk_x, chunk_y);
|
||||
}
|
||||
|
||||
GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) {
|
||||
if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) {
|
||||
return nullptr;
|
||||
}
|
||||
return chunks[chunk_y * chunks_x + chunk_x].get();
|
||||
}
|
||||
|
||||
const GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) const {
|
||||
if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) {
|
||||
return nullptr;
|
||||
}
|
||||
return chunks[chunk_y * chunks_x + chunk_x].get();
|
||||
}
|
||||
|
||||
UIGridPoint& ChunkManager::at(int x, int y) {
|
||||
GridChunk* chunk = getChunkForCell(x, y);
|
||||
if (!chunk) {
|
||||
// Return a static dummy point for out-of-bounds access
|
||||
// This matches the original behavior of UIGrid::at()
|
||||
static UIGridPoint dummy;
|
||||
return dummy;
|
||||
}
|
||||
|
||||
// Convert to local coordinates within chunk
|
||||
int local_x = x % GridChunk::CHUNK_SIZE;
|
||||
int local_y = y % GridChunk::CHUNK_SIZE;
|
||||
|
||||
// Mark chunk dirty when accessed for modification
|
||||
chunk->markDirty();
|
||||
|
||||
return chunk->at(local_x, local_y);
|
||||
}
|
||||
|
||||
const UIGridPoint& ChunkManager::at(int x, int y) const {
|
||||
const GridChunk* chunk = getChunkForCell(x, y);
|
||||
if (!chunk) {
|
||||
static UIGridPoint dummy;
|
||||
return dummy;
|
||||
}
|
||||
|
||||
int local_x = x % GridChunk::CHUNK_SIZE;
|
||||
int local_y = y % GridChunk::CHUNK_SIZE;
|
||||
|
||||
return chunk->at(local_x, local_y);
|
||||
}
|
||||
|
||||
void ChunkManager::markAllDirty() {
|
||||
for (auto& chunk : chunks) {
|
||||
chunk->markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GridChunk*> ChunkManager::getVisibleChunks(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge) {
|
||||
std::vector<GridChunk*> visible;
|
||||
visible.reserve(chunks.size()); // Pre-allocate for worst case
|
||||
|
||||
for (auto& chunk : chunks) {
|
||||
if (chunk->isVisible(left_edge, top_edge, right_edge, bottom_edge)) {
|
||||
visible.push_back(chunk.get());
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
void ChunkManager::resize(int new_grid_x, int new_grid_y) {
|
||||
// For now, simple rebuild - could be optimized to preserve data
|
||||
grid_x = new_grid_x;
|
||||
grid_y = new_grid_y;
|
||||
|
||||
chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
|
||||
chunks.clear();
|
||||
chunks.reserve(chunks_x * chunks_y);
|
||||
|
||||
for (int cy = 0; cy < chunks_y; ++cy) {
|
||||
for (int cx = 0; cx < chunks_x; ++cx) {
|
||||
int world_x = cx * GridChunk::CHUNK_SIZE;
|
||||
int world_y = cy * GridChunk::CHUNK_SIZE;
|
||||
int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x);
|
||||
int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y);
|
||||
|
||||
chunks.push_back(std::make_unique<GridChunk>(
|
||||
cx, cy, chunk_width, chunk_height, world_x, world_y, parent_grid
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int ChunkManager::dirtyChunks() const {
|
||||
int count = 0;
|
||||
for (const auto& chunk : chunks) {
|
||||
if (chunk->dirty) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
108
src/GridChunk.h
|
|
@ -1,108 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "UIGridPoint.h"
|
||||
|
||||
// Forward declarations
|
||||
class UIGrid;
|
||||
class PyTexture;
|
||||
|
||||
/**
|
||||
* #123 - Grid chunk for sub-grid data storage
|
||||
* #150 - Rendering removed; layers now handle all rendering
|
||||
*
|
||||
* Each chunk represents a CHUNK_SIZE x CHUNK_SIZE portion of the grid.
|
||||
* Chunks store GridPoint data for pathfinding and game logic.
|
||||
*/
|
||||
class GridChunk {
|
||||
public:
|
||||
// Compile-time configurable chunk size (power of 2 recommended)
|
||||
static constexpr int CHUNK_SIZE = 64;
|
||||
|
||||
// Position of this chunk in chunk coordinates
|
||||
int chunk_x, chunk_y;
|
||||
|
||||
// Actual dimensions (may be less than CHUNK_SIZE at grid edges)
|
||||
int width, height;
|
||||
|
||||
// World position (in cell coordinates)
|
||||
int world_x, world_y;
|
||||
|
||||
// Cell data for this chunk (pathfinding properties only)
|
||||
std::vector<UIGridPoint> cells;
|
||||
|
||||
// Dirty flag (for layer sync if needed)
|
||||
bool dirty;
|
||||
|
||||
// Parent grid reference
|
||||
UIGrid* parent_grid;
|
||||
|
||||
// Constructor
|
||||
GridChunk(int chunk_x, int chunk_y, int width, int height,
|
||||
int world_x, int world_y, UIGrid* parent);
|
||||
|
||||
// Access cell at local chunk coordinates
|
||||
UIGridPoint& at(int local_x, int local_y);
|
||||
const UIGridPoint& at(int local_x, int local_y) const;
|
||||
|
||||
// Mark chunk as dirty
|
||||
void markDirty();
|
||||
|
||||
// Get pixel bounds of this chunk in world coordinates
|
||||
sf::FloatRect getWorldBounds(int cell_width, int cell_height) const;
|
||||
|
||||
// Check if chunk overlaps with viewport
|
||||
bool isVisible(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages a 2D array of chunks for a grid
|
||||
*/
|
||||
class ChunkManager {
|
||||
public:
|
||||
// Dimensions in chunks
|
||||
int chunks_x, chunks_y;
|
||||
|
||||
// Grid dimensions in cells
|
||||
int grid_x, grid_y;
|
||||
|
||||
// All chunks (row-major order)
|
||||
std::vector<std::unique_ptr<GridChunk>> chunks;
|
||||
|
||||
// Parent grid
|
||||
UIGrid* parent_grid;
|
||||
|
||||
// Constructor - creates chunks for given grid dimensions
|
||||
ChunkManager(int grid_x, int grid_y, UIGrid* parent);
|
||||
|
||||
// Get chunk containing cell (x, y)
|
||||
GridChunk* getChunkForCell(int x, int y);
|
||||
const GridChunk* getChunkForCell(int x, int y) const;
|
||||
|
||||
// Get chunk at chunk coordinates
|
||||
GridChunk* getChunk(int chunk_x, int chunk_y);
|
||||
const GridChunk* getChunk(int chunk_x, int chunk_y) const;
|
||||
|
||||
// Access cell at grid coordinates (routes through chunk)
|
||||
UIGridPoint& at(int x, int y);
|
||||
const UIGridPoint& at(int x, int y) const;
|
||||
|
||||
// Mark all chunks dirty (for full rebuild)
|
||||
void markAllDirty();
|
||||
|
||||
// Get chunks that overlap with viewport
|
||||
std::vector<GridChunk*> getVisibleChunks(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge);
|
||||
|
||||
// Resize grid (rebuilds chunks)
|
||||
void resize(int new_grid_x, int new_grid_y);
|
||||
|
||||
// Get total number of chunks
|
||||
int totalChunks() const { return chunks_x * chunks_y; }
|
||||
|
||||
// Get number of dirty chunks
|
||||
int dirtyChunks() const;
|
||||
};
|
||||
|
|
@ -1,794 +0,0 @@
|
|||
#include "GridLayers.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyColor.h"
|
||||
#include "PyTexture.h"
|
||||
#include <sstream>
|
||||
|
||||
// =============================================================================
|
||||
// GridLayer base class
|
||||
// =============================================================================
|
||||
|
||||
GridLayer::GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent)
|
||||
: type(type), z_index(z_index), grid_x(grid_x), grid_y(grid_y),
|
||||
parent_grid(parent), visible(true),
|
||||
dirty(true), texture_initialized(false),
|
||||
cached_cell_width(0), cached_cell_height(0)
|
||||
{}
|
||||
|
||||
void GridLayer::markDirty() {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void GridLayer::ensureTextureSize(int cell_width, int cell_height) {
|
||||
// Check if we need to resize/create the texture
|
||||
unsigned int required_width = grid_x * cell_width;
|
||||
unsigned int required_height = grid_y * cell_height;
|
||||
|
||||
// Maximum texture size limit (prevent excessive memory usage)
|
||||
const unsigned int MAX_TEXTURE_SIZE = 4096;
|
||||
if (required_width > MAX_TEXTURE_SIZE) required_width = MAX_TEXTURE_SIZE;
|
||||
if (required_height > MAX_TEXTURE_SIZE) required_height = MAX_TEXTURE_SIZE;
|
||||
|
||||
// Skip if already properly sized
|
||||
if (texture_initialized &&
|
||||
cached_texture.getSize().x == required_width &&
|
||||
cached_texture.getSize().y == required_height &&
|
||||
cached_cell_width == cell_width &&
|
||||
cached_cell_height == cell_height) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create or resize the texture (SFML uses .create() not .resize())
|
||||
if (!cached_texture.create(required_width, required_height)) {
|
||||
// Creation failed - texture will remain uninitialized
|
||||
texture_initialized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
cached_cell_width = cell_width;
|
||||
cached_cell_height = cell_height;
|
||||
texture_initialized = true;
|
||||
dirty = true; // Force re-render after resize
|
||||
|
||||
// Setup the sprite to use the texture
|
||||
cached_sprite.setTexture(cached_texture.getTexture());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ColorLayer implementation
|
||||
// =============================================================================
|
||||
|
||||
ColorLayer::ColorLayer(int z_index, int grid_x, int grid_y, UIGrid* parent)
|
||||
: GridLayer(GridLayerType::Color, z_index, grid_x, grid_y, parent),
|
||||
colors(grid_x * grid_y, sf::Color::Transparent)
|
||||
{}
|
||||
|
||||
sf::Color& ColorLayer::at(int x, int y) {
|
||||
return colors[y * grid_x + x];
|
||||
}
|
||||
|
||||
const sf::Color& ColorLayer::at(int x, int y) const {
|
||||
return colors[y * grid_x + x];
|
||||
}
|
||||
|
||||
void ColorLayer::fill(const sf::Color& color) {
|
||||
std::fill(colors.begin(), colors.end(), color);
|
||||
markDirty(); // #148 - Mark for re-render
|
||||
}
|
||||
|
||||
void ColorLayer::resize(int new_grid_x, int new_grid_y) {
|
||||
std::vector<sf::Color> new_colors(new_grid_x * new_grid_y, sf::Color::Transparent);
|
||||
|
||||
// Copy existing data
|
||||
int copy_x = std::min(grid_x, new_grid_x);
|
||||
int copy_y = std::min(grid_y, new_grid_y);
|
||||
for (int y = 0; y < copy_y; ++y) {
|
||||
for (int x = 0; x < copy_x; ++x) {
|
||||
new_colors[y * new_grid_x + x] = colors[y * grid_x + x];
|
||||
}
|
||||
}
|
||||
|
||||
colors = std::move(new_colors);
|
||||
grid_x = new_grid_x;
|
||||
grid_y = new_grid_y;
|
||||
|
||||
// #148 - Invalidate cached texture (will be resized on next render)
|
||||
texture_initialized = false;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
// #148 - Render all cells to cached texture (called when dirty)
|
||||
void ColorLayer::renderToTexture(int cell_width, int cell_height) {
|
||||
ensureTextureSize(cell_width, cell_height);
|
||||
if (!texture_initialized) return;
|
||||
|
||||
cached_texture.clear(sf::Color::Transparent);
|
||||
|
||||
sf::RectangleShape rect;
|
||||
rect.setSize(sf::Vector2f(cell_width, cell_height));
|
||||
rect.setOutlineThickness(0);
|
||||
|
||||
// Render all cells to cached texture (no zoom - 1:1 pixel mapping)
|
||||
for (int x = 0; x < grid_x; ++x) {
|
||||
for (int y = 0; y < grid_y; ++y) {
|
||||
const sf::Color& color = at(x, y);
|
||||
if (color.a == 0) continue; // Skip fully transparent
|
||||
|
||||
rect.setPosition(sf::Vector2f(x * cell_width, y * cell_height));
|
||||
rect.setFillColor(color);
|
||||
cached_texture.draw(rect);
|
||||
}
|
||||
}
|
||||
|
||||
cached_texture.display();
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
void ColorLayer::render(sf::RenderTarget& target,
|
||||
float left_spritepixels, float top_spritepixels,
|
||||
int left_edge, int top_edge, int x_limit, int y_limit,
|
||||
float zoom, int cell_width, int cell_height) {
|
||||
if (!visible) return;
|
||||
|
||||
// #148 - Use cached texture rendering
|
||||
// Re-render to texture only if dirty
|
||||
if (dirty || !texture_initialized) {
|
||||
renderToTexture(cell_width, cell_height);
|
||||
}
|
||||
|
||||
if (!texture_initialized) {
|
||||
// Fallback to direct rendering if texture creation failed
|
||||
sf::RectangleShape rect;
|
||||
rect.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
||||
rect.setOutlineThickness(0);
|
||||
|
||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); x < x_limit; ++x) {
|
||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); y < y_limit; ++y) {
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
||||
|
||||
const sf::Color& color = at(x, y);
|
||||
if (color.a == 0) continue;
|
||||
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(x * cell_width - left_spritepixels) * zoom,
|
||||
(y * cell_height - top_spritepixels) * zoom
|
||||
);
|
||||
|
||||
rect.setPosition(pixel_pos);
|
||||
rect.setFillColor(color);
|
||||
target.draw(rect);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Blit visible portion of cached texture with zoom applied
|
||||
// Calculate source rectangle (unzoomed pixel coordinates in cached texture)
|
||||
int src_left = std::max(0, (int)left_spritepixels);
|
||||
int src_top = std::max(0, (int)top_spritepixels);
|
||||
int src_width = std::min((int)cached_texture.getSize().x - src_left,
|
||||
(int)((x_limit - left_edge + 2) * cell_width));
|
||||
int src_height = std::min((int)cached_texture.getSize().y - src_top,
|
||||
(int)((y_limit - top_edge + 2) * cell_height));
|
||||
|
||||
if (src_width <= 0 || src_height <= 0) return;
|
||||
|
||||
// Set texture rect for visible portion
|
||||
cached_sprite.setTextureRect(sf::IntRect({src_left, src_top}, {src_width, src_height}));
|
||||
|
||||
// Position in target (offset for partial cell visibility)
|
||||
float dest_x = (src_left - left_spritepixels) * zoom;
|
||||
float dest_y = (src_top - top_spritepixels) * zoom;
|
||||
cached_sprite.setPosition(sf::Vector2f(dest_x, dest_y));
|
||||
|
||||
// Apply zoom via scale
|
||||
cached_sprite.setScale(sf::Vector2f(zoom, zoom));
|
||||
|
||||
target.draw(cached_sprite);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TileLayer implementation
|
||||
// =============================================================================
|
||||
|
||||
TileLayer::TileLayer(int z_index, int grid_x, int grid_y, UIGrid* parent,
|
||||
std::shared_ptr<PyTexture> texture)
|
||||
: GridLayer(GridLayerType::Tile, z_index, grid_x, grid_y, parent),
|
||||
tiles(grid_x * grid_y, -1), // -1 = no tile
|
||||
texture(texture)
|
||||
{}
|
||||
|
||||
int& TileLayer::at(int x, int y) {
|
||||
return tiles[y * grid_x + x];
|
||||
}
|
||||
|
||||
int TileLayer::at(int x, int y) const {
|
||||
return tiles[y * grid_x + x];
|
||||
}
|
||||
|
||||
void TileLayer::fill(int tile_index) {
|
||||
std::fill(tiles.begin(), tiles.end(), tile_index);
|
||||
markDirty(); // #148 - Mark for re-render
|
||||
}
|
||||
|
||||
void TileLayer::resize(int new_grid_x, int new_grid_y) {
|
||||
std::vector<int> new_tiles(new_grid_x * new_grid_y, -1);
|
||||
|
||||
// Copy existing data
|
||||
int copy_x = std::min(grid_x, new_grid_x);
|
||||
int copy_y = std::min(grid_y, new_grid_y);
|
||||
for (int y = 0; y < copy_y; ++y) {
|
||||
for (int x = 0; x < copy_x; ++x) {
|
||||
new_tiles[y * new_grid_x + x] = tiles[y * grid_x + x];
|
||||
}
|
||||
}
|
||||
|
||||
tiles = std::move(new_tiles);
|
||||
grid_x = new_grid_x;
|
||||
grid_y = new_grid_y;
|
||||
|
||||
// #148 - Invalidate cached texture (will be resized on next render)
|
||||
texture_initialized = false;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
// #148 - Render all cells to cached texture (called when dirty)
|
||||
void TileLayer::renderToTexture(int cell_width, int cell_height) {
|
||||
ensureTextureSize(cell_width, cell_height);
|
||||
if (!texture_initialized || !texture) return;
|
||||
|
||||
cached_texture.clear(sf::Color::Transparent);
|
||||
|
||||
// Render all tiles to cached texture (no zoom - 1:1 pixel mapping)
|
||||
for (int x = 0; x < grid_x; ++x) {
|
||||
for (int y = 0; y < grid_y; ++y) {
|
||||
int tile_index = at(x, y);
|
||||
if (tile_index < 0) continue; // No tile
|
||||
|
||||
auto pixel_pos = sf::Vector2f(x * cell_width, y * cell_height);
|
||||
sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(1.0f, 1.0f));
|
||||
cached_texture.draw(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
cached_texture.display();
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
void TileLayer::render(sf::RenderTarget& target,
|
||||
float left_spritepixels, float top_spritepixels,
|
||||
int left_edge, int top_edge, int x_limit, int y_limit,
|
||||
float zoom, int cell_width, int cell_height) {
|
||||
if (!visible || !texture) return;
|
||||
|
||||
// #148 - Use cached texture rendering
|
||||
// Re-render to texture only if dirty
|
||||
if (dirty || !texture_initialized) {
|
||||
renderToTexture(cell_width, cell_height);
|
||||
}
|
||||
|
||||
if (!texture_initialized) {
|
||||
// Fallback to direct rendering if texture creation failed
|
||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); x < x_limit; ++x) {
|
||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); y < y_limit; ++y) {
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
||||
|
||||
int tile_index = at(x, y);
|
||||
if (tile_index < 0) continue;
|
||||
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(x * cell_width - left_spritepixels) * zoom,
|
||||
(y * cell_height - top_spritepixels) * zoom
|
||||
);
|
||||
|
||||
sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(zoom, zoom));
|
||||
target.draw(sprite);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Blit visible portion of cached texture with zoom applied
|
||||
// Calculate source rectangle (unzoomed pixel coordinates in cached texture)
|
||||
int src_left = std::max(0, (int)left_spritepixels);
|
||||
int src_top = std::max(0, (int)top_spritepixels);
|
||||
int src_width = std::min((int)cached_texture.getSize().x - src_left,
|
||||
(int)((x_limit - left_edge + 2) * cell_width));
|
||||
int src_height = std::min((int)cached_texture.getSize().y - src_top,
|
||||
(int)((y_limit - top_edge + 2) * cell_height));
|
||||
|
||||
if (src_width <= 0 || src_height <= 0) return;
|
||||
|
||||
// Set texture rect for visible portion
|
||||
cached_sprite.setTextureRect(sf::IntRect({src_left, src_top}, {src_width, src_height}));
|
||||
|
||||
// Position in target (offset for partial cell visibility)
|
||||
float dest_x = (src_left - left_spritepixels) * zoom;
|
||||
float dest_y = (src_top - top_spritepixels) * zoom;
|
||||
cached_sprite.setPosition(sf::Vector2f(dest_x, dest_y));
|
||||
|
||||
// Apply zoom via scale
|
||||
cached_sprite.setScale(sf::Vector2f(zoom, zoom));
|
||||
|
||||
target.draw(cached_sprite);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Python API - ColorLayer
|
||||
// =============================================================================
|
||||
|
||||
PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = {
|
||||
{"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS,
|
||||
"at(x, y) -> Color\n\nGet the color at cell position (x, y)."},
|
||||
{"set", (PyCFunction)PyGridLayerAPI::ColorLayer_set, METH_VARARGS,
|
||||
"set(x, y, color)\n\nSet the color at cell position (x, y)."},
|
||||
{"fill", (PyCFunction)PyGridLayerAPI::ColorLayer_fill, METH_VARARGS,
|
||||
"fill(color)\n\nFill the entire layer with the specified color."},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyGetSetDef PyGridLayerAPI::ColorLayer_getsetters[] = {
|
||||
{"z_index", (getter)PyGridLayerAPI::ColorLayer_get_z_index,
|
||||
(setter)PyGridLayerAPI::ColorLayer_set_z_index,
|
||||
"Layer z-order. Negative values render below entities.", NULL},
|
||||
{"visible", (getter)PyGridLayerAPI::ColorLayer_get_visible,
|
||||
(setter)PyGridLayerAPI::ColorLayer_set_visible,
|
||||
"Whether the layer is rendered.", NULL},
|
||||
{"grid_size", (getter)PyGridLayerAPI::ColorLayer_get_grid_size, NULL,
|
||||
"Layer dimensions as (width, height) tuple.", NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"z_index", "grid_size", NULL};
|
||||
int z_index = -1;
|
||||
PyObject* grid_size_obj = nullptr;
|
||||
int grid_x = 0, grid_y = 0;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iO", const_cast<char**>(kwlist),
|
||||
&z_index, &grid_size_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Parse grid_size if provided
|
||||
if (grid_size_obj && grid_size_obj != Py_None) {
|
||||
if (!PyTuple_Check(grid_size_obj) || PyTuple_Size(grid_size_obj) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size must be a (width, height) tuple");
|
||||
return -1;
|
||||
}
|
||||
grid_x = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 0));
|
||||
grid_y = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 1));
|
||||
if (PyErr_Occurred()) return -1;
|
||||
}
|
||||
|
||||
// Create the layer (will be attached to grid via add_layer)
|
||||
self->data = std::make_shared<ColorLayer>(z_index, grid_x, grid_y, nullptr);
|
||||
self->grid.reset();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args) {
|
||||
int x, y;
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
|
||||
PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const sf::Color& color = self->data->at(x, y);
|
||||
|
||||
// Return as mcrfpy.Color
|
||||
auto* color_type = (PyTypeObject*)PyObject_GetAttrString(
|
||||
PyImport_ImportModule("mcrfpy"), "Color");
|
||||
if (!color_type) return NULL;
|
||||
|
||||
PyColorObject* color_obj = (PyColorObject*)color_type->tp_alloc(color_type, 0);
|
||||
Py_DECREF(color_type);
|
||||
if (!color_obj) return NULL;
|
||||
|
||||
color_obj->data = color;
|
||||
return (PyObject*)color_obj;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* args) {
|
||||
int x, y;
|
||||
PyObject* color_obj;
|
||||
if (!PyArg_ParseTuple(args, "iiO", &x, &y, &color_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
|
||||
PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse color
|
||||
sf::Color color;
|
||||
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||
if (!mcrfpy_module) return NULL;
|
||||
|
||||
auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color");
|
||||
Py_DECREF(mcrfpy_module);
|
||||
if (!color_type) return NULL;
|
||||
|
||||
if (PyObject_IsInstance(color_obj, color_type)) {
|
||||
color = ((PyColorObject*)color_obj)->data;
|
||||
} else if (PyTuple_Check(color_obj)) {
|
||||
int r, g, b, a = 255;
|
||||
if (!PyArg_ParseTuple(color_obj, "iii|i", &r, &g, &b, &a)) {
|
||||
Py_DECREF(color_type);
|
||||
return NULL;
|
||||
}
|
||||
color = sf::Color(r, g, b, a);
|
||||
} else {
|
||||
Py_DECREF(color_type);
|
||||
PyErr_SetString(PyExc_TypeError, "color must be a Color object or (r, g, b[, a]) tuple");
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(color_type);
|
||||
|
||||
self->data->at(x, y) = color;
|
||||
self->data->markDirty(); // #148 - Mark for re-render
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::ColorLayer_fill(PyColorLayerObject* self, PyObject* args) {
|
||||
PyObject* color_obj;
|
||||
if (!PyArg_ParseTuple(args, "O", &color_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse color
|
||||
sf::Color color;
|
||||
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||
if (!mcrfpy_module) return NULL;
|
||||
|
||||
auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color");
|
||||
Py_DECREF(mcrfpy_module);
|
||||
if (!color_type) return NULL;
|
||||
|
||||
if (PyObject_IsInstance(color_obj, color_type)) {
|
||||
color = ((PyColorObject*)color_obj)->data;
|
||||
} else if (PyTuple_Check(color_obj)) {
|
||||
int r, g, b, a = 255;
|
||||
if (!PyArg_ParseTuple(color_obj, "iii|i", &r, &g, &b, &a)) {
|
||||
Py_DECREF(color_type);
|
||||
return NULL;
|
||||
}
|
||||
color = sf::Color(r, g, b, a);
|
||||
} else {
|
||||
Py_DECREF(color_type);
|
||||
PyErr_SetString(PyExc_TypeError, "color must be a Color object or (r, g, b[, a]) tuple");
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(color_type);
|
||||
|
||||
self->data->fill(color);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::ColorLayer_get_z_index(PyColorLayerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
return PyLong_FromLong(self->data->z_index);
|
||||
}
|
||||
|
||||
int PyGridLayerAPI::ColorLayer_set_z_index(PyColorLayerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return -1;
|
||||
}
|
||||
long z = PyLong_AsLong(value);
|
||||
if (PyErr_Occurred()) return -1;
|
||||
self->data->z_index = z;
|
||||
// TODO: Trigger re-sort in parent grid
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::ColorLayer_get_visible(PyColorLayerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
return PyBool_FromLong(self->data->visible);
|
||||
}
|
||||
|
||||
int PyGridLayerAPI::ColorLayer_set_visible(PyColorLayerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return -1;
|
||||
}
|
||||
int v = PyObject_IsTrue(value);
|
||||
if (v < 0) return -1;
|
||||
self->data->visible = v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::ColorLayer_get_grid_size(PyColorLayerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) {
|
||||
std::ostringstream ss;
|
||||
if (!self->data) {
|
||||
ss << "<ColorLayer (invalid)>";
|
||||
} else {
|
||||
ss << "<ColorLayer z_index=" << self->data->z_index
|
||||
<< " size=(" << self->data->grid_x << "x" << self->data->grid_y << ")"
|
||||
<< " visible=" << (self->data->visible ? "True" : "False") << ">";
|
||||
}
|
||||
return PyUnicode_FromString(ss.str().c_str());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Python API - TileLayer
|
||||
// =============================================================================
|
||||
|
||||
PyMethodDef PyGridLayerAPI::TileLayer_methods[] = {
|
||||
{"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS,
|
||||
"at(x, y) -> int\n\nGet the tile index at cell position (x, y). Returns -1 if no tile."},
|
||||
{"set", (PyCFunction)PyGridLayerAPI::TileLayer_set, METH_VARARGS,
|
||||
"set(x, y, index)\n\nSet the tile index at cell position (x, y). Use -1 for no tile."},
|
||||
{"fill", (PyCFunction)PyGridLayerAPI::TileLayer_fill, METH_VARARGS,
|
||||
"fill(index)\n\nFill the entire layer with the specified tile index."},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyGetSetDef PyGridLayerAPI::TileLayer_getsetters[] = {
|
||||
{"z_index", (getter)PyGridLayerAPI::TileLayer_get_z_index,
|
||||
(setter)PyGridLayerAPI::TileLayer_set_z_index,
|
||||
"Layer z-order. Negative values render below entities.", NULL},
|
||||
{"visible", (getter)PyGridLayerAPI::TileLayer_get_visible,
|
||||
(setter)PyGridLayerAPI::TileLayer_set_visible,
|
||||
"Whether the layer is rendered.", NULL},
|
||||
{"texture", (getter)PyGridLayerAPI::TileLayer_get_texture,
|
||||
(setter)PyGridLayerAPI::TileLayer_set_texture,
|
||||
"Texture atlas for tile sprites.", NULL},
|
||||
{"grid_size", (getter)PyGridLayerAPI::TileLayer_get_grid_size, NULL,
|
||||
"Layer dimensions as (width, height) tuple.", NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"z_index", "texture", "grid_size", NULL};
|
||||
int z_index = -1;
|
||||
PyObject* texture_obj = nullptr;
|
||||
PyObject* grid_size_obj = nullptr;
|
||||
int grid_x = 0, grid_y = 0;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iOO", const_cast<char**>(kwlist),
|
||||
&z_index, &texture_obj, &grid_size_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Parse texture
|
||||
std::shared_ptr<PyTexture> texture;
|
||||
if (texture_obj && texture_obj != Py_None) {
|
||||
// Check if it's a PyTexture
|
||||
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||
if (!mcrfpy_module) return -1;
|
||||
|
||||
auto* texture_type = PyObject_GetAttrString(mcrfpy_module, "Texture");
|
||||
Py_DECREF(mcrfpy_module);
|
||||
if (!texture_type) return -1;
|
||||
|
||||
if (PyObject_IsInstance(texture_obj, texture_type)) {
|
||||
texture = ((PyTextureObject*)texture_obj)->data;
|
||||
} else {
|
||||
Py_DECREF(texture_type);
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a Texture object");
|
||||
return -1;
|
||||
}
|
||||
Py_DECREF(texture_type);
|
||||
}
|
||||
|
||||
// Parse grid_size if provided
|
||||
if (grid_size_obj && grid_size_obj != Py_None) {
|
||||
if (!PyTuple_Check(grid_size_obj) || PyTuple_Size(grid_size_obj) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size must be a (width, height) tuple");
|
||||
return -1;
|
||||
}
|
||||
grid_x = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 0));
|
||||
grid_y = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 1));
|
||||
if (PyErr_Occurred()) return -1;
|
||||
}
|
||||
|
||||
// Create the layer
|
||||
self->data = std::make_shared<TileLayer>(z_index, grid_x, grid_y, nullptr, texture);
|
||||
self->grid.reset();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args) {
|
||||
int x, y;
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
|
||||
PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PyLong_FromLong(self->data->at(x, y));
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args) {
|
||||
int x, y, index;
|
||||
if (!PyArg_ParseTuple(args, "iii", &x, &y, &index)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
|
||||
PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->data->at(x, y) = index;
|
||||
self->data->markDirty(); // #148 - Mark for re-render
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_fill(PyTileLayerObject* self, PyObject* args) {
|
||||
int index;
|
||||
if (!PyArg_ParseTuple(args, "i", &index)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->data->fill(index);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_get_z_index(PyTileLayerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
return PyLong_FromLong(self->data->z_index);
|
||||
}
|
||||
|
||||
int PyGridLayerAPI::TileLayer_set_z_index(PyTileLayerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return -1;
|
||||
}
|
||||
long z = PyLong_AsLong(value);
|
||||
if (PyErr_Occurred()) return -1;
|
||||
self->data->z_index = z;
|
||||
// TODO: Trigger re-sort in parent grid
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_get_visible(PyTileLayerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
return PyBool_FromLong(self->data->visible);
|
||||
}
|
||||
|
||||
int PyGridLayerAPI::TileLayer_set_visible(PyTileLayerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return -1;
|
||||
}
|
||||
int v = PyObject_IsTrue(value);
|
||||
if (v < 0) return -1;
|
||||
self->data->visible = v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_get_texture(PyTileLayerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!self->data->texture) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
auto* texture_type = (PyTypeObject*)PyObject_GetAttrString(
|
||||
PyImport_ImportModule("mcrfpy"), "Texture");
|
||||
if (!texture_type) return NULL;
|
||||
|
||||
PyTextureObject* tex_obj = (PyTextureObject*)texture_type->tp_alloc(texture_type, 0);
|
||||
Py_DECREF(texture_type);
|
||||
if (!tex_obj) return NULL;
|
||||
|
||||
tex_obj->data = self->data->texture;
|
||||
return (PyObject*)tex_obj;
|
||||
}
|
||||
|
||||
int PyGridLayerAPI::TileLayer_set_texture(PyTileLayerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (value == Py_None) {
|
||||
self->data->texture.reset();
|
||||
self->data->markDirty(); // #148 - Mark for re-render
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||
if (!mcrfpy_module) return -1;
|
||||
|
||||
auto* texture_type = PyObject_GetAttrString(mcrfpy_module, "Texture");
|
||||
Py_DECREF(mcrfpy_module);
|
||||
if (!texture_type) return -1;
|
||||
|
||||
if (!PyObject_IsInstance(value, texture_type)) {
|
||||
Py_DECREF(texture_type);
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a Texture object or None");
|
||||
return -1;
|
||||
}
|
||||
Py_DECREF(texture_type);
|
||||
|
||||
self->data->texture = ((PyTextureObject*)value)->data;
|
||||
self->data->markDirty(); // #148 - Mark for re-render
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_get_grid_size(PyTileLayerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_repr(PyTileLayerObject* self) {
|
||||
std::ostringstream ss;
|
||||
if (!self->data) {
|
||||
ss << "<TileLayer (invalid)>";
|
||||
} else {
|
||||
ss << "<TileLayer z_index=" << self->data->z_index
|
||||
<< " size=(" << self->data->grid_x << "x" << self->data->grid_y << ")"
|
||||
<< " visible=" << (self->data->visible ? "True" : "False")
|
||||
<< " texture=" << (self->data->texture ? "set" : "None") << ">";
|
||||
}
|
||||
return PyUnicode_FromString(ss.str().c_str());
|
||||
}
|
||||
244
src/GridLayers.h
|
|
@ -1,244 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "structmember.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
// Forward declarations
|
||||
class UIGrid;
|
||||
class PyTexture;
|
||||
|
||||
// Include PyTexture.h for PyTextureObject (typedef, not struct)
|
||||
#include "PyTexture.h"
|
||||
|
||||
// Layer type enumeration
|
||||
enum class GridLayerType {
|
||||
Color,
|
||||
Tile
|
||||
};
|
||||
|
||||
// Abstract base class for grid layers
|
||||
class GridLayer {
|
||||
public:
|
||||
GridLayerType type;
|
||||
std::string name; // #150 - Layer name for GridPoint property access
|
||||
int z_index; // Negative = below entities, >= 0 = above entities
|
||||
int grid_x, grid_y; // Dimensions
|
||||
UIGrid* parent_grid; // Parent grid reference
|
||||
bool visible; // Visibility flag
|
||||
|
||||
// #148 - Dirty flag and RenderTexture caching
|
||||
bool dirty; // True if layer needs re-render
|
||||
sf::RenderTexture cached_texture; // Cached layer content
|
||||
sf::Sprite cached_sprite; // Sprite for blitting cached texture
|
||||
bool texture_initialized; // True if RenderTexture has been created
|
||||
int cached_cell_width, cached_cell_height; // Cell size used for cached texture
|
||||
|
||||
GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent);
|
||||
virtual ~GridLayer() = default;
|
||||
|
||||
// Mark layer as needing re-render
|
||||
void markDirty();
|
||||
|
||||
// Ensure cached texture is properly sized for current grid dimensions
|
||||
void ensureTextureSize(int cell_width, int cell_height);
|
||||
|
||||
// Render the layer content to the cached texture (called when dirty)
|
||||
virtual void renderToTexture(int cell_width, int cell_height) = 0;
|
||||
|
||||
// Render the layer to a RenderTarget with the given transformation parameters
|
||||
// Uses cached texture if available, only re-renders when dirty
|
||||
virtual void render(sf::RenderTarget& target,
|
||||
float left_spritepixels, float top_spritepixels,
|
||||
int left_edge, int top_edge, int x_limit, int y_limit,
|
||||
float zoom, int cell_width, int cell_height) = 0;
|
||||
|
||||
// Resize the layer (reallocates storage)
|
||||
virtual void resize(int new_grid_x, int new_grid_y) = 0;
|
||||
};
|
||||
|
||||
// Color layer - stores RGBA color per cell
|
||||
class ColorLayer : public GridLayer {
|
||||
public:
|
||||
std::vector<sf::Color> colors;
|
||||
|
||||
ColorLayer(int z_index, int grid_x, int grid_y, UIGrid* parent);
|
||||
|
||||
// Access color at position
|
||||
sf::Color& at(int x, int y);
|
||||
const sf::Color& at(int x, int y) const;
|
||||
|
||||
// Fill entire layer with a color
|
||||
void fill(const sf::Color& color);
|
||||
|
||||
// #148 - Render all content to cached texture
|
||||
void renderToTexture(int cell_width, int cell_height) override;
|
||||
|
||||
void render(sf::RenderTarget& target,
|
||||
float left_spritepixels, float top_spritepixels,
|
||||
int left_edge, int top_edge, int x_limit, int y_limit,
|
||||
float zoom, int cell_width, int cell_height) override;
|
||||
|
||||
void resize(int new_grid_x, int new_grid_y) override;
|
||||
};
|
||||
|
||||
// Tile layer - stores sprite index per cell with texture reference
|
||||
class TileLayer : public GridLayer {
|
||||
public:
|
||||
std::vector<int> tiles; // Sprite indices (-1 = no tile)
|
||||
std::shared_ptr<PyTexture> texture;
|
||||
|
||||
TileLayer(int z_index, int grid_x, int grid_y, UIGrid* parent,
|
||||
std::shared_ptr<PyTexture> texture = nullptr);
|
||||
|
||||
// Access tile index at position
|
||||
int& at(int x, int y);
|
||||
int at(int x, int y) const;
|
||||
|
||||
// Fill entire layer with a tile index
|
||||
void fill(int tile_index);
|
||||
|
||||
// #148 - Render all content to cached texture
|
||||
void renderToTexture(int cell_width, int cell_height) override;
|
||||
|
||||
void render(sf::RenderTarget& target,
|
||||
float left_spritepixels, float top_spritepixels,
|
||||
int left_edge, int top_edge, int x_limit, int y_limit,
|
||||
float zoom, int cell_width, int cell_height) override;
|
||||
|
||||
void resize(int new_grid_x, int new_grid_y) override;
|
||||
};
|
||||
|
||||
// Python wrapper types
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<GridLayer> data;
|
||||
std::shared_ptr<UIGrid> grid; // Parent grid reference
|
||||
} PyGridLayerObject;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<ColorLayer> data;
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
} PyColorLayerObject;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<TileLayer> data;
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
} PyTileLayerObject;
|
||||
|
||||
// Python API classes
|
||||
class PyGridLayerAPI {
|
||||
public:
|
||||
// ColorLayer methods
|
||||
static int ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args);
|
||||
static PyObject* ColorLayer_set(PyColorLayerObject* self, PyObject* args);
|
||||
static PyObject* ColorLayer_fill(PyColorLayerObject* self, PyObject* args);
|
||||
static PyObject* ColorLayer_get_z_index(PyColorLayerObject* self, void* closure);
|
||||
static int ColorLayer_set_z_index(PyColorLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* ColorLayer_get_visible(PyColorLayerObject* self, void* closure);
|
||||
static int ColorLayer_set_visible(PyColorLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* ColorLayer_get_grid_size(PyColorLayerObject* self, void* closure);
|
||||
static PyObject* ColorLayer_repr(PyColorLayerObject* self);
|
||||
|
||||
// TileLayer methods
|
||||
static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args);
|
||||
static PyObject* TileLayer_set(PyTileLayerObject* self, PyObject* args);
|
||||
static PyObject* TileLayer_fill(PyTileLayerObject* self, PyObject* args);
|
||||
static PyObject* TileLayer_get_z_index(PyTileLayerObject* self, void* closure);
|
||||
static int TileLayer_set_z_index(PyTileLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* TileLayer_get_visible(PyTileLayerObject* self, void* closure);
|
||||
static int TileLayer_set_visible(PyTileLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* TileLayer_get_texture(PyTileLayerObject* self, void* closure);
|
||||
static int TileLayer_set_texture(PyTileLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* TileLayer_get_grid_size(PyTileLayerObject* self, void* closure);
|
||||
static PyObject* TileLayer_repr(PyTileLayerObject* self);
|
||||
|
||||
// Method and getset arrays
|
||||
static PyMethodDef ColorLayer_methods[];
|
||||
static PyGetSetDef ColorLayer_getsetters[];
|
||||
static PyMethodDef TileLayer_methods[];
|
||||
static PyGetSetDef TileLayer_getsetters[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
// ColorLayer type
|
||||
static PyTypeObject PyColorLayerType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.ColorLayer",
|
||||
.tp_basicsize = sizeof(PyColorLayerObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self) {
|
||||
PyColorLayerObject* obj = (PyColorLayerObject*)self;
|
||||
obj->data.reset();
|
||||
obj->grid.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("ColorLayer(z_index=-1, grid_size=None)\n\n"
|
||||
"A grid layer that stores RGBA colors per cell.\n\n"
|
||||
"Args:\n"
|
||||
" z_index (int): Render order. Negative = below entities. Default: -1\n"
|
||||
" grid_size (tuple): Dimensions as (width, height). Default: parent grid size\n\n"
|
||||
"Attributes:\n"
|
||||
" z_index (int): Layer z-order relative to entities\n"
|
||||
" visible (bool): Whether layer is rendered\n"
|
||||
" grid_size (tuple): Layer dimensions (read-only)\n\n"
|
||||
"Methods:\n"
|
||||
" at(x, y): Get color at cell position\n"
|
||||
" set(x, y, color): Set color at cell position\n"
|
||||
" fill(color): Fill entire layer with color"),
|
||||
.tp_methods = PyGridLayerAPI::ColorLayer_methods,
|
||||
.tp_getset = PyGridLayerAPI::ColorLayer_getsetters,
|
||||
.tp_init = (initproc)PyGridLayerAPI::ColorLayer_init,
|
||||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
|
||||
PyColorLayerObject* self = (PyColorLayerObject*)type->tp_alloc(type, 0);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
|
||||
// TileLayer type
|
||||
static PyTypeObject PyTileLayerType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.TileLayer",
|
||||
.tp_basicsize = sizeof(PyTileLayerObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self) {
|
||||
PyTileLayerObject* obj = (PyTileLayerObject*)self;
|
||||
obj->data.reset();
|
||||
obj->grid.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("TileLayer(z_index=-1, texture=None, grid_size=None)\n\n"
|
||||
"A grid layer that stores sprite indices per cell.\n\n"
|
||||
"Args:\n"
|
||||
" z_index (int): Render order. Negative = below entities. Default: -1\n"
|
||||
" texture (Texture): Sprite atlas for tile rendering. Default: None\n"
|
||||
" grid_size (tuple): Dimensions as (width, height). Default: parent grid size\n\n"
|
||||
"Attributes:\n"
|
||||
" z_index (int): Layer z-order relative to entities\n"
|
||||
" visible (bool): Whether layer is rendered\n"
|
||||
" texture (Texture): Tile sprite atlas\n"
|
||||
" grid_size (tuple): Layer dimensions (read-only)\n\n"
|
||||
"Methods:\n"
|
||||
" at(x, y): Get tile index at cell position\n"
|
||||
" set(x, y, index): Set tile index at cell position\n"
|
||||
" fill(index): Fill entire layer with tile index"),
|
||||
.tp_methods = PyGridLayerAPI::TileLayer_methods,
|
||||
.tp_getset = PyGridLayerAPI::TileLayer_getsetters,
|
||||
.tp_init = (initproc)PyGridLayerAPI::TileLayer_init,
|
||||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
|
||||
PyTileLayerObject* self = (PyTileLayerObject*)type->tp_alloc(type, 0);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
#include "ImGuiConsole.h"
|
||||
#include "imgui.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include <Python.h>
|
||||
#include <sstream>
|
||||
|
||||
// Static member initialization
|
||||
bool ImGuiConsole::enabled = true;
|
||||
|
||||
ImGuiConsole::ImGuiConsole() {
|
||||
addOutput("McRogueFace Python Console", false);
|
||||
addOutput("Type Python commands and press Enter to execute.", false);
|
||||
addOutput("", false);
|
||||
}
|
||||
|
||||
void ImGuiConsole::toggle() {
|
||||
if (enabled) {
|
||||
visible = !visible;
|
||||
if (visible) {
|
||||
// Focus input when opening
|
||||
ImGui::SetWindowFocus("Console");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ImGuiConsole::wantsKeyboardInput() const {
|
||||
return visible && enabled;
|
||||
}
|
||||
|
||||
void ImGuiConsole::addOutput(const std::string& text, bool isError) {
|
||||
// Split text by newlines and add each line separately
|
||||
std::istringstream stream(text);
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
outputHistory.push_back({line, isError, false});
|
||||
}
|
||||
|
||||
// Trim history if too long
|
||||
while (outputHistory.size() > MAX_HISTORY) {
|
||||
outputHistory.pop_front();
|
||||
}
|
||||
|
||||
scrollToBottom = true;
|
||||
}
|
||||
|
||||
void ImGuiConsole::executeCommand(const std::string& command) {
|
||||
if (command.empty()) return;
|
||||
|
||||
// Add command to output with >>> prefix
|
||||
outputHistory.push_back({">>> " + command, false, true});
|
||||
|
||||
// Add to command history
|
||||
commandHistory.push_back(command);
|
||||
historyIndex = -1;
|
||||
|
||||
// Capture Python output
|
||||
// Redirect stdout/stderr to capture output
|
||||
std::string captureCode = R"(
|
||||
import sys
|
||||
import io
|
||||
_console_stdout = io.StringIO()
|
||||
_console_stderr = io.StringIO()
|
||||
_old_stdout = sys.stdout
|
||||
_old_stderr = sys.stderr
|
||||
sys.stdout = _console_stdout
|
||||
sys.stderr = _console_stderr
|
||||
)";
|
||||
|
||||
std::string restoreCode = R"(
|
||||
sys.stdout = _old_stdout
|
||||
sys.stderr = _old_stderr
|
||||
_stdout_val = _console_stdout.getvalue()
|
||||
_stderr_val = _console_stderr.getvalue()
|
||||
)";
|
||||
|
||||
// Set up capture
|
||||
PyRun_SimpleString(captureCode.c_str());
|
||||
|
||||
// Try to evaluate as expression first (for things like "2+2")
|
||||
PyObject* main_module = PyImport_AddModule("__main__");
|
||||
PyObject* main_dict = PyModule_GetDict(main_module);
|
||||
|
||||
// First try eval (for expressions that return values)
|
||||
PyObject* result = PyRun_String(command.c_str(), Py_eval_input, main_dict, main_dict);
|
||||
bool showedResult = false;
|
||||
|
||||
if (result == nullptr) {
|
||||
// Clear the error from eval attempt
|
||||
PyErr_Clear();
|
||||
|
||||
// Try exec (for statements)
|
||||
result = PyRun_String(command.c_str(), Py_file_input, main_dict, main_dict);
|
||||
|
||||
if (result == nullptr) {
|
||||
// Real error - capture it
|
||||
PyErr_Print(); // This prints to stderr which we're capturing
|
||||
}
|
||||
} else if (result != Py_None) {
|
||||
// Expression returned a non-None value - show its repr
|
||||
PyObject* repr = PyObject_Repr(result);
|
||||
if (repr) {
|
||||
const char* repr_str = PyUnicode_AsUTF8(repr);
|
||||
if (repr_str) {
|
||||
addOutput(repr_str, false);
|
||||
showedResult = true;
|
||||
}
|
||||
Py_DECREF(repr);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(result);
|
||||
|
||||
// Restore stdout/stderr
|
||||
PyRun_SimpleString(restoreCode.c_str());
|
||||
|
||||
// Get captured stdout (only if we didn't already show a result)
|
||||
PyObject* stdout_val = PyObject_GetAttrString(main_module, "_stdout_val");
|
||||
if (stdout_val && PyUnicode_Check(stdout_val)) {
|
||||
const char* stdout_str = PyUnicode_AsUTF8(stdout_val);
|
||||
if (stdout_str && strlen(stdout_str) > 0) {
|
||||
addOutput(stdout_str, false);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(stdout_val);
|
||||
|
||||
// Get captured stderr
|
||||
PyObject* stderr_val = PyObject_GetAttrString(main_module, "_stderr_val");
|
||||
if (stderr_val && PyUnicode_Check(stderr_val)) {
|
||||
const char* stderr_str = PyUnicode_AsUTF8(stderr_val);
|
||||
if (stderr_str && strlen(stderr_str) > 0) {
|
||||
addOutput(stderr_str, true);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(stderr_val);
|
||||
|
||||
// Clean up temporary variables
|
||||
PyRun_SimpleString("del _console_stdout, _console_stderr, _old_stdout, _old_stderr, _stdout_val, _stderr_val");
|
||||
|
||||
scrollToBottom = true;
|
||||
}
|
||||
|
||||
void ImGuiConsole::render() {
|
||||
if (!visible || !enabled) return;
|
||||
|
||||
// Set up console window
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y * 0.4f), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
|
||||
if (!ImGui::Begin("Console", &visible, flags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Output area (scrollable, no horizontal scrollbar - use word wrap)
|
||||
float footerHeight = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing();
|
||||
ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footerHeight), false, ImGuiWindowFlags_None);
|
||||
|
||||
// Render output lines with word wrap
|
||||
for (const auto& line : outputHistory) {
|
||||
if (line.isInput) {
|
||||
// User input - yellow/gold color
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.9f, 0.4f, 1.0f));
|
||||
} else if (line.isError) {
|
||||
// Error - red color
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
||||
} else {
|
||||
// Normal output - default color
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.8f, 0.8f, 1.0f));
|
||||
}
|
||||
|
||||
ImGui::TextWrapped("%s", line.text.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom when new content is added
|
||||
if (scrollToBottom || ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
}
|
||||
scrollToBottom = false;
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
// Input line
|
||||
ImGui::Separator();
|
||||
|
||||
// Input field
|
||||
ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue |
|
||||
ImGuiInputTextFlags_CallbackHistory |
|
||||
ImGuiInputTextFlags_CallbackCompletion;
|
||||
|
||||
bool reclaimFocus = false;
|
||||
|
||||
// Custom callback for history navigation
|
||||
auto callback = [](ImGuiInputTextCallbackData* data) -> int {
|
||||
ImGuiConsole* console = static_cast<ImGuiConsole*>(data->UserData);
|
||||
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) {
|
||||
if (console->commandHistory.empty()) return 0;
|
||||
|
||||
if (data->EventKey == ImGuiKey_UpArrow) {
|
||||
if (console->historyIndex < 0) {
|
||||
console->historyIndex = static_cast<int>(console->commandHistory.size()) - 1;
|
||||
} else if (console->historyIndex > 0) {
|
||||
console->historyIndex--;
|
||||
}
|
||||
} else if (data->EventKey == ImGuiKey_DownArrow) {
|
||||
if (console->historyIndex >= 0) {
|
||||
console->historyIndex++;
|
||||
if (console->historyIndex >= static_cast<int>(console->commandHistory.size())) {
|
||||
console->historyIndex = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update input buffer
|
||||
if (console->historyIndex >= 0 && console->historyIndex < static_cast<int>(console->commandHistory.size())) {
|
||||
const std::string& historyEntry = console->commandHistory[console->historyIndex];
|
||||
data->DeleteChars(0, data->BufTextLen);
|
||||
data->InsertChars(0, historyEntry.c_str());
|
||||
} else {
|
||||
data->DeleteChars(0, data->BufTextLen);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
ImGui::PushItemWidth(-1); // Full width
|
||||
if (ImGui::InputText("##Input", inputBuffer, sizeof(inputBuffer), inputFlags, callback, this)) {
|
||||
std::string command(inputBuffer);
|
||||
inputBuffer[0] = '\0';
|
||||
executeCommand(command);
|
||||
reclaimFocus = true;
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
// Keep focus on input
|
||||
ImGui::SetItemDefaultFocus();
|
||||
if (reclaimFocus || (visible && !ImGui::IsAnyItemActive())) {
|
||||
ImGui::SetKeyboardFocusHere(-1);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
|
||||
/**
|
||||
* @brief ImGui-based debug console for Python REPL
|
||||
*
|
||||
* Provides an overlay console that can execute Python code
|
||||
* without blocking the main game loop. Activated by grave/tilde key.
|
||||
*/
|
||||
class ImGuiConsole {
|
||||
public:
|
||||
ImGuiConsole();
|
||||
|
||||
// Core functionality
|
||||
void render(); // Render the console UI
|
||||
void toggle(); // Toggle visibility
|
||||
bool isVisible() const { return visible; }
|
||||
void setVisible(bool v) { visible = v; }
|
||||
|
||||
// Configuration (for Python API)
|
||||
static bool isEnabled() { return enabled; }
|
||||
static void setEnabled(bool e) { enabled = e; }
|
||||
|
||||
// Input handling
|
||||
bool wantsKeyboardInput() const; // Returns true if ImGui wants keyboard
|
||||
|
||||
private:
|
||||
void executeCommand(const std::string& command);
|
||||
void addOutput(const std::string& text, bool isError = false);
|
||||
|
||||
// State
|
||||
bool visible = false;
|
||||
static bool enabled; // Global enable/disable (for shipping games)
|
||||
|
||||
// Input buffer
|
||||
char inputBuffer[1024] = {0};
|
||||
|
||||
// Output history
|
||||
struct OutputLine {
|
||||
std::string text;
|
||||
bool isError;
|
||||
bool isInput; // True if this was user input (for styling)
|
||||
};
|
||||
std::deque<OutputLine> outputHistory;
|
||||
static constexpr size_t MAX_HISTORY = 500;
|
||||
|
||||
// Command history for up/down navigation
|
||||
std::vector<std::string> commandHistory;
|
||||
int historyIndex = -1;
|
||||
|
||||
// Scroll state
|
||||
bool scrollToBottom = true;
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
#ifndef MCRFPY_DOC_H
|
||||
#define MCRFPY_DOC_H
|
||||
|
||||
// Section builders for documentation
|
||||
#define MCRF_SIG(params, ret) params " -> " ret "\n\n"
|
||||
#define MCRF_DESC(text) text "\n\n"
|
||||
#define MCRF_ARGS_START "Args:\n"
|
||||
#define MCRF_ARG(name, desc) " " name ": " desc "\n"
|
||||
#define MCRF_RETURNS(text) "\nReturns:\n " text "\n"
|
||||
#define MCRF_RAISES(exc, desc) "\nRaises:\n " exc ": " desc "\n"
|
||||
#define MCRF_NOTE(text) "\nNote:\n " text "\n"
|
||||
|
||||
// Link to external documentation
|
||||
// Format: MCRF_LINK("docs/file.md", "Link Text")
|
||||
// Parsers detect this pattern and format per output type
|
||||
#define MCRF_LINK(ref, text) "\nSee also: " text " (" ref ")\n"
|
||||
|
||||
// Main documentation macros
|
||||
#define MCRF_METHOD_DOC(name, sig, desc, ...) \
|
||||
name sig desc __VA_ARGS__
|
||||
|
||||
#define MCRF_FUNCTION(name, ...) \
|
||||
MCRF_METHOD_DOC(#name, __VA_ARGS__)
|
||||
|
||||
#define MCRF_METHOD(cls, name, ...) \
|
||||
MCRF_METHOD_DOC(#name, __VA_ARGS__)
|
||||
|
||||
#define MCRF_PROPERTY(name, desc) \
|
||||
desc
|
||||
|
||||
#endif // MCRFPY_DOC_H
|
||||
|
|
@ -1,324 +0,0 @@
|
|||
#include "McRFPy_Libtcod.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "UIGrid.h"
|
||||
#include <vector>
|
||||
|
||||
// Helper function to get UIGrid from Python object
|
||||
static UIGrid* get_grid_from_pyobject(PyObject* obj) {
|
||||
auto grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
|
||||
if (!grid_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Could not find Grid type");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!PyObject_IsInstance(obj, (PyObject*)grid_type)) {
|
||||
Py_DECREF(grid_type);
|
||||
PyErr_SetString(PyExc_TypeError, "First argument must be a Grid object");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Py_DECREF(grid_type);
|
||||
PyUIGridObject* pygrid = (PyUIGridObject*)obj;
|
||||
return pygrid->data.get();
|
||||
}
|
||||
|
||||
// Field of View computation
|
||||
static PyObject* McRFPy_Libtcod::compute_fov(PyObject* self, PyObject* args) {
|
||||
PyObject* grid_obj;
|
||||
int x, y, radius;
|
||||
int light_walls = 1;
|
||||
int algorithm = FOV_BASIC;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "Oiii|ii", &grid_obj, &x, &y, &radius,
|
||||
&light_walls, &algorithm)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
UIGrid* grid = get_grid_from_pyobject(grid_obj);
|
||||
if (!grid) return NULL;
|
||||
|
||||
// Compute FOV using grid's method
|
||||
grid->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
|
||||
|
||||
// Return list of visible cells
|
||||
PyObject* visible_list = PyList_New(0);
|
||||
for (int gy = 0; gy < grid->grid_y; gy++) {
|
||||
for (int gx = 0; gx < grid->grid_x; gx++) {
|
||||
if (grid->isInFOV(gx, gy)) {
|
||||
PyObject* pos = Py_BuildValue("(ii)", gx, gy);
|
||||
PyList_Append(visible_list, pos);
|
||||
Py_DECREF(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return visible_list;
|
||||
}
|
||||
|
||||
// A* Pathfinding
|
||||
static PyObject* McRFPy_Libtcod::find_path(PyObject* self, PyObject* args) {
|
||||
PyObject* grid_obj;
|
||||
int x1, y1, x2, y2;
|
||||
float diagonal_cost = 1.41f;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "Oiiii|f", &grid_obj, &x1, &y1, &x2, &y2, &diagonal_cost)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
UIGrid* grid = get_grid_from_pyobject(grid_obj);
|
||||
if (!grid) return NULL;
|
||||
|
||||
// Get path from grid
|
||||
std::vector<std::pair<int, int>> path = grid->findPath(x1, y1, x2, y2, diagonal_cost);
|
||||
|
||||
// Convert to Python list
|
||||
PyObject* path_list = PyList_New(path.size());
|
||||
for (size_t i = 0; i < path.size(); i++) {
|
||||
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
|
||||
PyList_SetItem(path_list, i, pos); // steals reference
|
||||
}
|
||||
|
||||
return path_list;
|
||||
}
|
||||
|
||||
// Line drawing algorithm
|
||||
static PyObject* McRFPy_Libtcod::line(PyObject* self, PyObject* args) {
|
||||
int x1, y1, x2, y2;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "iiii", &x1, &y1, &x2, &y2)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Use TCOD's line algorithm
|
||||
TCODLine::init(x1, y1, x2, y2);
|
||||
|
||||
PyObject* line_list = PyList_New(0);
|
||||
int x, y;
|
||||
|
||||
// Step through line
|
||||
while (!TCODLine::step(&x, &y)) {
|
||||
PyObject* pos = Py_BuildValue("(ii)", x, y);
|
||||
PyList_Append(line_list, pos);
|
||||
Py_DECREF(pos);
|
||||
}
|
||||
|
||||
return line_list;
|
||||
}
|
||||
|
||||
// Line iterator (generator-like function)
|
||||
static PyObject* McRFPy_Libtcod::line_iter(PyObject* self, PyObject* args) {
|
||||
// For simplicity, just call line() for now
|
||||
// A proper implementation would create an iterator object
|
||||
return line(self, args);
|
||||
}
|
||||
|
||||
// Dijkstra pathfinding
|
||||
static PyObject* McRFPy_Libtcod::dijkstra_new(PyObject* self, PyObject* args) {
|
||||
PyObject* grid_obj;
|
||||
float diagonal_cost = 1.41f;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "O|f", &grid_obj, &diagonal_cost)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
UIGrid* grid = get_grid_from_pyobject(grid_obj);
|
||||
if (!grid) return NULL;
|
||||
|
||||
// For now, just return the grid object since Dijkstra is part of the grid
|
||||
Py_INCREF(grid_obj);
|
||||
return grid_obj;
|
||||
}
|
||||
|
||||
static PyObject* McRFPy_Libtcod::dijkstra_compute(PyObject* self, PyObject* args) {
|
||||
PyObject* grid_obj;
|
||||
int root_x, root_y;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &root_x, &root_y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
UIGrid* grid = get_grid_from_pyobject(grid_obj);
|
||||
if (!grid) return NULL;
|
||||
|
||||
grid->computeDijkstra(root_x, root_y);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* McRFPy_Libtcod::dijkstra_get_distance(PyObject* self, PyObject* args) {
|
||||
PyObject* grid_obj;
|
||||
int x, y;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
UIGrid* grid = get_grid_from_pyobject(grid_obj);
|
||||
if (!grid) return NULL;
|
||||
|
||||
float distance = grid->getDijkstraDistance(x, y);
|
||||
if (distance < 0) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
return PyFloat_FromDouble(distance);
|
||||
}
|
||||
|
||||
static PyObject* McRFPy_Libtcod::dijkstra_path_to(PyObject* self, PyObject* args) {
|
||||
PyObject* grid_obj;
|
||||
int x, y;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
UIGrid* grid = get_grid_from_pyobject(grid_obj);
|
||||
if (!grid) return NULL;
|
||||
|
||||
std::vector<std::pair<int, int>> path = grid->getDijkstraPath(x, y);
|
||||
|
||||
PyObject* path_list = PyList_New(path.size());
|
||||
for (size_t i = 0; i < path.size(); i++) {
|
||||
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
|
||||
PyList_SetItem(path_list, i, pos); // steals reference
|
||||
}
|
||||
|
||||
return path_list;
|
||||
}
|
||||
|
||||
// Add FOV algorithm constants to module
|
||||
static PyObject* McRFPy_Libtcod::add_fov_constants(PyObject* module) {
|
||||
// FOV algorithms
|
||||
PyModule_AddIntConstant(module, "FOV_BASIC", FOV_BASIC);
|
||||
PyModule_AddIntConstant(module, "FOV_DIAMOND", FOV_DIAMOND);
|
||||
PyModule_AddIntConstant(module, "FOV_SHADOW", FOV_SHADOW);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_0", FOV_PERMISSIVE_0);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_1", FOV_PERMISSIVE_1);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_2", FOV_PERMISSIVE_2);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_3", FOV_PERMISSIVE_3);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_4", FOV_PERMISSIVE_4);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_5", FOV_PERMISSIVE_5);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_6", FOV_PERMISSIVE_6);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_7", FOV_PERMISSIVE_7);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_8", FOV_PERMISSIVE_8);
|
||||
PyModule_AddIntConstant(module, "FOV_RESTRICTIVE", FOV_RESTRICTIVE);
|
||||
PyModule_AddIntConstant(module, "FOV_SYMMETRIC_SHADOWCAST", FOV_SYMMETRIC_SHADOWCAST);
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
// Method definitions
|
||||
static PyMethodDef libtcodMethods[] = {
|
||||
{"compute_fov", McRFPy_Libtcod::compute_fov, METH_VARARGS,
|
||||
"compute_fov(grid, x, y, radius, light_walls=True, algorithm=FOV_BASIC)\n\n"
|
||||
"Compute field of view from a position.\n\n"
|
||||
"Args:\n"
|
||||
" grid: Grid object to compute FOV on\n"
|
||||
" x, y: Origin position\n"
|
||||
" radius: Maximum sight radius\n"
|
||||
" light_walls: Whether walls are lit when in FOV\n"
|
||||
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_SHADOW, etc.)\n\n"
|
||||
"Returns:\n"
|
||||
" List of (x, y) tuples for visible cells"},
|
||||
|
||||
{"find_path", McRFPy_Libtcod::find_path, METH_VARARGS,
|
||||
"find_path(grid, x1, y1, x2, y2, diagonal_cost=1.41)\n\n"
|
||||
"Find shortest path between two points using A*.\n\n"
|
||||
"Args:\n"
|
||||
" grid: Grid object to pathfind on\n"
|
||||
" x1, y1: Starting position\n"
|
||||
" x2, y2: Target position\n"
|
||||
" diagonal_cost: Cost of diagonal movement\n\n"
|
||||
"Returns:\n"
|
||||
" List of (x, y) tuples representing the path, or empty list if no path exists"},
|
||||
|
||||
{"line", McRFPy_Libtcod::line, METH_VARARGS,
|
||||
"line(x1, y1, x2, y2)\n\n"
|
||||
"Get cells along a line using Bresenham's algorithm.\n\n"
|
||||
"Args:\n"
|
||||
" x1, y1: Starting position\n"
|
||||
" x2, y2: Ending position\n\n"
|
||||
"Returns:\n"
|
||||
" List of (x, y) tuples along the line"},
|
||||
|
||||
{"line_iter", McRFPy_Libtcod::line_iter, METH_VARARGS,
|
||||
"line_iter(x1, y1, x2, y2)\n\n"
|
||||
"Iterate over cells along a line.\n\n"
|
||||
"Args:\n"
|
||||
" x1, y1: Starting position\n"
|
||||
" x2, y2: Ending position\n\n"
|
||||
"Returns:\n"
|
||||
" Iterator of (x, y) tuples along the line"},
|
||||
|
||||
{"dijkstra_new", McRFPy_Libtcod::dijkstra_new, METH_VARARGS,
|
||||
"dijkstra_new(grid, diagonal_cost=1.41)\n\n"
|
||||
"Create a Dijkstra pathfinding context for a grid.\n\n"
|
||||
"Args:\n"
|
||||
" grid: Grid object to use for pathfinding\n"
|
||||
" diagonal_cost: Cost of diagonal movement\n\n"
|
||||
"Returns:\n"
|
||||
" Grid object configured for Dijkstra pathfinding"},
|
||||
|
||||
{"dijkstra_compute", McRFPy_Libtcod::dijkstra_compute, METH_VARARGS,
|
||||
"dijkstra_compute(grid, root_x, root_y)\n\n"
|
||||
"Compute Dijkstra distance map from root position.\n\n"
|
||||
"Args:\n"
|
||||
" grid: Grid object with Dijkstra context\n"
|
||||
" root_x, root_y: Root position to compute distances from"},
|
||||
|
||||
{"dijkstra_get_distance", McRFPy_Libtcod::dijkstra_get_distance, METH_VARARGS,
|
||||
"dijkstra_get_distance(grid, x, y)\n\n"
|
||||
"Get distance from root to a position.\n\n"
|
||||
"Args:\n"
|
||||
" grid: Grid object with computed Dijkstra map\n"
|
||||
" x, y: Position to get distance for\n\n"
|
||||
"Returns:\n"
|
||||
" Float distance or None if position is invalid/unreachable"},
|
||||
|
||||
{"dijkstra_path_to", McRFPy_Libtcod::dijkstra_path_to, METH_VARARGS,
|
||||
"dijkstra_path_to(grid, x, y)\n\n"
|
||||
"Get shortest path from position to Dijkstra root.\n\n"
|
||||
"Args:\n"
|
||||
" grid: Grid object with computed Dijkstra map\n"
|
||||
" x, y: Starting position\n\n"
|
||||
"Returns:\n"
|
||||
" List of (x, y) tuples representing the path to root"},
|
||||
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
// Module definition
|
||||
static PyModuleDef libtcodModule = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
"mcrfpy.libtcod",
|
||||
"TCOD-compatible algorithms for field of view, pathfinding, and line drawing.\n\n"
|
||||
"This module provides access to TCOD's algorithms integrated with McRogueFace grids.\n"
|
||||
"Unlike the original TCOD, these functions work directly with Grid objects.\n\n"
|
||||
"FOV Algorithms:\n"
|
||||
" FOV_BASIC - Basic circular FOV\n"
|
||||
" FOV_SHADOW - Shadow casting (recommended)\n"
|
||||
" FOV_DIAMOND - Diamond-shaped FOV\n"
|
||||
" FOV_PERMISSIVE_0 through FOV_PERMISSIVE_8 - Permissive variants\n"
|
||||
" FOV_RESTRICTIVE - Most restrictive FOV\n"
|
||||
" FOV_SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\n"
|
||||
"Example:\n"
|
||||
" import mcrfpy\n"
|
||||
" from mcrfpy import libtcod\n\n"
|
||||
" grid = mcrfpy.Grid(50, 50)\n"
|
||||
" visible = libtcod.compute_fov(grid, 25, 25, 10)\n"
|
||||
" path = libtcod.find_path(grid, 0, 0, 49, 49)",
|
||||
-1,
|
||||
libtcodMethods
|
||||
};
|
||||
|
||||
// Module initialization
|
||||
PyObject* McRFPy_Libtcod::init_libtcod_module() {
|
||||
PyObject* m = PyModule_Create(&libtcodModule);
|
||||
if (m == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Add FOV algorithm constants
|
||||
add_fov_constants(m);
|
||||
|
||||
return m;
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <libtcod.h>
|
||||
|
||||
namespace McRFPy_Libtcod
|
||||
{
|
||||
// Field of View algorithms
|
||||
static PyObject* compute_fov(PyObject* self, PyObject* args);
|
||||
|
||||
// Pathfinding
|
||||
static PyObject* find_path(PyObject* self, PyObject* args);
|
||||
static PyObject* dijkstra_new(PyObject* self, PyObject* args);
|
||||
static PyObject* dijkstra_compute(PyObject* self, PyObject* args);
|
||||
static PyObject* dijkstra_get_distance(PyObject* self, PyObject* args);
|
||||
static PyObject* dijkstra_path_to(PyObject* self, PyObject* args);
|
||||
|
||||
// Line algorithms
|
||||
static PyObject* line(PyObject* self, PyObject* args);
|
||||
static PyObject* line_iter(PyObject* self, PyObject* args);
|
||||
|
||||
// FOV algorithm constants
|
||||
static PyObject* add_fov_constants(PyObject* module);
|
||||
|
||||
// Module initialization
|
||||
PyObject* init_libtcod_module();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
#include "Profiler.h"
|
||||
#include <iostream>
|
||||
|
||||
ProfilingLogger::ProfilingLogger()
|
||||
: headers_written(false)
|
||||
{
|
||||
}
|
||||
|
||||
ProfilingLogger::~ProfilingLogger() {
|
||||
close();
|
||||
}
|
||||
|
||||
bool ProfilingLogger::open(const std::string& filename, const std::vector<std::string>& columns) {
|
||||
column_names = columns;
|
||||
file.open(filename);
|
||||
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "Failed to open profiling log file: " << filename << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write CSV header
|
||||
for (size_t i = 0; i < columns.size(); ++i) {
|
||||
file << columns[i];
|
||||
if (i < columns.size() - 1) {
|
||||
file << ",";
|
||||
}
|
||||
}
|
||||
file << "\n";
|
||||
file.flush();
|
||||
|
||||
headers_written = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProfilingLogger::writeRow(const std::vector<float>& values) {
|
||||
if (!file.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.size() != column_names.size()) {
|
||||
std::cerr << "ProfilingLogger: value count (" << values.size()
|
||||
<< ") doesn't match column count (" << column_names.size() << ")" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < values.size(); ++i) {
|
||||
file << values[i];
|
||||
if (i < values.size() - 1) {
|
||||
file << ",";
|
||||
}
|
||||
}
|
||||
file << "\n";
|
||||
}
|
||||
|
||||
void ProfilingLogger::close() {
|
||||
if (file.is_open()) {
|
||||
file.flush();
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
111
src/Profiler.h
|
|
@ -1,111 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <fstream>
|
||||
|
||||
/**
|
||||
* @brief Simple RAII-based profiling timer for measuring code execution time
|
||||
*
|
||||
* Usage:
|
||||
* float timing = 0.0f;
|
||||
* {
|
||||
* ScopedTimer timer(timing);
|
||||
* // ... code to profile ...
|
||||
* } // timing now contains elapsed milliseconds
|
||||
*/
|
||||
class ScopedTimer {
|
||||
private:
|
||||
std::chrono::high_resolution_clock::time_point start;
|
||||
float& target_ms;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Scoped Timer and start timing
|
||||
* @param target Reference to float that will receive elapsed time in milliseconds
|
||||
*/
|
||||
explicit ScopedTimer(float& target)
|
||||
: target_ms(target)
|
||||
{
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destructor automatically records elapsed time
|
||||
*/
|
||||
~ScopedTimer() {
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
target_ms = std::chrono::duration<float, std::milli>(end - start).count();
|
||||
}
|
||||
|
||||
// Prevent copying
|
||||
ScopedTimer(const ScopedTimer&) = delete;
|
||||
ScopedTimer& operator=(const ScopedTimer&) = delete;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Accumulating timer that adds elapsed time to existing value
|
||||
*
|
||||
* Useful for measuring total time across multiple calls in a single frame
|
||||
*/
|
||||
class AccumulatingTimer {
|
||||
private:
|
||||
std::chrono::high_resolution_clock::time_point start;
|
||||
float& target_ms;
|
||||
|
||||
public:
|
||||
explicit AccumulatingTimer(float& target)
|
||||
: target_ms(target)
|
||||
{
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
}
|
||||
|
||||
~AccumulatingTimer() {
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
target_ms += std::chrono::duration<float, std::milli>(end - start).count();
|
||||
}
|
||||
|
||||
AccumulatingTimer(const AccumulatingTimer&) = delete;
|
||||
AccumulatingTimer& operator=(const AccumulatingTimer&) = delete;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief CSV profiling data logger for batch analysis
|
||||
*
|
||||
* Writes profiling data to CSV file for later analysis with Python/pandas/Excel
|
||||
*/
|
||||
class ProfilingLogger {
|
||||
private:
|
||||
std::ofstream file;
|
||||
bool headers_written;
|
||||
std::vector<std::string> column_names;
|
||||
|
||||
public:
|
||||
ProfilingLogger();
|
||||
~ProfilingLogger();
|
||||
|
||||
/**
|
||||
* @brief Open a CSV file for writing profiling data
|
||||
* @param filename Path to CSV file
|
||||
* @param columns Column names for the CSV header
|
||||
* @return true if file opened successfully
|
||||
*/
|
||||
bool open(const std::string& filename, const std::vector<std::string>& columns);
|
||||
|
||||
/**
|
||||
* @brief Write a row of profiling data
|
||||
* @param values Data values (must match column count)
|
||||
*/
|
||||
void writeRow(const std::vector<float>& values);
|
||||
|
||||
/**
|
||||
* @brief Close the file and flush data
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* @brief Check if logger is ready to write
|
||||
*/
|
||||
bool isOpen() const { return file.is_open(); }
|
||||
};
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
#include "GameEngine.h"
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
GameEngine::ProfilerOverlay::ProfilerOverlay(sf::Font& fontRef)
|
||||
: font(fontRef), visible(false), updateInterval(10), frameCounter(0)
|
||||
{
|
||||
text.setFont(font);
|
||||
text.setCharacterSize(14);
|
||||
text.setFillColor(sf::Color::White);
|
||||
text.setPosition(10.0f, 10.0f);
|
||||
|
||||
// Semi-transparent dark background
|
||||
background.setFillColor(sf::Color(0, 0, 0, 180));
|
||||
background.setPosition(5.0f, 5.0f);
|
||||
}
|
||||
|
||||
void GameEngine::ProfilerOverlay::toggle() {
|
||||
visible = !visible;
|
||||
}
|
||||
|
||||
void GameEngine::ProfilerOverlay::setVisible(bool vis) {
|
||||
visible = vis;
|
||||
}
|
||||
|
||||
bool GameEngine::ProfilerOverlay::isVisible() const {
|
||||
return visible;
|
||||
}
|
||||
|
||||
sf::Color GameEngine::ProfilerOverlay::getPerformanceColor(float frameTimeMs) {
|
||||
if (frameTimeMs < 16.6f) {
|
||||
return sf::Color::Green; // 60+ FPS
|
||||
} else if (frameTimeMs < 33.3f) {
|
||||
return sf::Color::Yellow; // 30-60 FPS
|
||||
} else {
|
||||
return sf::Color::Red; // <30 FPS
|
||||
}
|
||||
}
|
||||
|
||||
std::string GameEngine::ProfilerOverlay::formatFloat(float value, int precision) {
|
||||
std::stringstream ss;
|
||||
ss << std::fixed << std::setprecision(precision) << value;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string GameEngine::ProfilerOverlay::formatPercentage(float part, float total) {
|
||||
if (total <= 0.0f) return "0%";
|
||||
float pct = (part / total) * 100.0f;
|
||||
return formatFloat(pct, 0) + "%";
|
||||
}
|
||||
|
||||
void GameEngine::ProfilerOverlay::update(const ProfilingMetrics& metrics) {
|
||||
if (!visible) return;
|
||||
|
||||
// Only update text every N frames to reduce overhead
|
||||
frameCounter++;
|
||||
if (frameCounter < updateInterval) {
|
||||
return;
|
||||
}
|
||||
frameCounter = 0;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "McRogueFace Performance Monitor\n";
|
||||
ss << "================================\n";
|
||||
|
||||
// Frame time and FPS
|
||||
float frameMs = metrics.avgFrameTime;
|
||||
ss << "FPS: " << metrics.fps << " (" << formatFloat(frameMs, 1) << "ms/frame)\n";
|
||||
|
||||
// Performance warning
|
||||
if (frameMs > 33.3f) {
|
||||
ss << "WARNING: Frame time exceeds 30 FPS target!\n";
|
||||
}
|
||||
|
||||
ss << "\n";
|
||||
|
||||
// Timing breakdown
|
||||
ss << "Frame Time Breakdown:\n";
|
||||
ss << " Grid Render: " << formatFloat(metrics.gridRenderTime, 1) << "ms ("
|
||||
<< formatPercentage(metrics.gridRenderTime, frameMs) << ")\n";
|
||||
ss << " Cells: " << metrics.gridCellsRendered << " rendered\n";
|
||||
ss << " Entities: " << metrics.entitiesRendered << " / " << metrics.totalEntities << " drawn\n";
|
||||
|
||||
if (metrics.fovOverlayTime > 0.01f) {
|
||||
ss << " FOV Overlay: " << formatFloat(metrics.fovOverlayTime, 1) << "ms\n";
|
||||
}
|
||||
|
||||
if (metrics.entityRenderTime > 0.01f) {
|
||||
ss << " Entity Render: " << formatFloat(metrics.entityRenderTime, 1) << "ms ("
|
||||
<< formatPercentage(metrics.entityRenderTime, frameMs) << ")\n";
|
||||
}
|
||||
|
||||
if (metrics.pythonScriptTime > 0.01f) {
|
||||
ss << " Python: " << formatFloat(metrics.pythonScriptTime, 1) << "ms ("
|
||||
<< formatPercentage(metrics.pythonScriptTime, frameMs) << ")\n";
|
||||
}
|
||||
|
||||
if (metrics.animationTime > 0.01f) {
|
||||
ss << " Animations: " << formatFloat(metrics.animationTime, 1) << "ms ("
|
||||
<< formatPercentage(metrics.animationTime, frameMs) << ")\n";
|
||||
}
|
||||
|
||||
ss << "\n";
|
||||
|
||||
// Other metrics
|
||||
ss << "Draw Calls: " << metrics.drawCalls << "\n";
|
||||
ss << "UI Elements: " << metrics.uiElements << " (" << metrics.visibleElements << " visible)\n";
|
||||
|
||||
// Calculate unaccounted time
|
||||
float accountedTime = metrics.gridRenderTime + metrics.entityRenderTime +
|
||||
metrics.pythonScriptTime + metrics.animationTime;
|
||||
float unaccountedTime = frameMs - accountedTime;
|
||||
|
||||
if (unaccountedTime > 1.0f) {
|
||||
ss << "\n";
|
||||
ss << "Other: " << formatFloat(unaccountedTime, 1) << "ms ("
|
||||
<< formatPercentage(unaccountedTime, frameMs) << ")\n";
|
||||
}
|
||||
|
||||
ss << "\n";
|
||||
ss << "Press F3 to hide this overlay";
|
||||
|
||||
text.setString(ss.str());
|
||||
|
||||
// Update background size to fit text
|
||||
sf::FloatRect textBounds = text.getLocalBounds();
|
||||
background.setSize(sf::Vector2f(textBounds.width + 20.0f, textBounds.height + 20.0f));
|
||||
}
|
||||
|
||||
void GameEngine::ProfilerOverlay::render(sf::RenderTarget& target) {
|
||||
if (!visible) return;
|
||||
|
||||
target.draw(background);
|
||||
target.draw(text);
|
||||
}
|
||||
|
|
@ -1,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}
|
||||
};
|
||||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -1,27 +1,10 @@
|
|||
#include "PyCallable.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "GameEngine.h"
|
||||
|
||||
PyCallable::PyCallable(PyObject* _target)
|
||||
{
|
||||
target = Py_XNewRef(_target);
|
||||
}
|
||||
|
||||
PyCallable::PyCallable(const PyCallable& other)
|
||||
{
|
||||
target = Py_XNewRef(other.target);
|
||||
}
|
||||
|
||||
PyCallable& PyCallable::operator=(const PyCallable& other)
|
||||
{
|
||||
if (this != &other) {
|
||||
PyObject* old_target = target;
|
||||
target = Py_XNewRef(other.target);
|
||||
Py_XDECREF(old_target);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
PyCallable::~PyCallable()
|
||||
{
|
||||
if (target)
|
||||
|
|
@ -33,11 +16,49 @@ PyObject* PyCallable::call(PyObject* args, PyObject* kwargs)
|
|||
return PyObject_Call(target, args, kwargs);
|
||||
}
|
||||
|
||||
bool PyCallable::isNone() const
|
||||
bool PyCallable::isNone()
|
||||
{
|
||||
return (target == Py_None || target == NULL);
|
||||
}
|
||||
|
||||
PyTimerCallable::PyTimerCallable(PyObject* _target, int _interval, int now)
|
||||
: PyCallable(_target), interval(_interval), last_ran(now)
|
||||
{}
|
||||
|
||||
PyTimerCallable::PyTimerCallable()
|
||||
: PyCallable(Py_None), interval(0), last_ran(0)
|
||||
{}
|
||||
|
||||
bool PyTimerCallable::hasElapsed(int now)
|
||||
{
|
||||
return now >= last_ran + interval;
|
||||
}
|
||||
|
||||
void PyTimerCallable::call(int now)
|
||||
{
|
||||
PyObject* args = Py_BuildValue("(i)", now);
|
||||
PyObject* retval = PyCallable::call(args, NULL);
|
||||
if (!retval)
|
||||
{
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
std::cout << PyUnicode_AsUTF8(PyObject_Repr(retval)) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
bool PyTimerCallable::test(int now)
|
||||
{
|
||||
if(hasElapsed(now))
|
||||
{
|
||||
call(now);
|
||||
last_ran = now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
PyClickCallable::PyClickCallable(PyObject* _target)
|
||||
: PyCallable(_target)
|
||||
|
|
@ -53,14 +74,9 @@ void PyClickCallable::call(sf::Vector2f mousepos, std::string button, std::strin
|
|||
PyObject* retval = PyCallable::call(args, NULL);
|
||||
if (!retval)
|
||||
{
|
||||
std::cerr << "Click callback raised an exception:" << std::endl;
|
||||
std::cout << "ClickCallable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
|
||||
// Check if we should exit on exception
|
||||
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
|
||||
McRFPy_API::signalPythonException();
|
||||
}
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "ClickCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
|
|
@ -88,14 +104,9 @@ void PyKeyCallable::call(std::string key, std::string action)
|
|||
PyObject* retval = PyCallable::call(args, NULL);
|
||||
if (!retval)
|
||||
{
|
||||
std::cerr << "Key callback raised an exception:" << std::endl;
|
||||
std::cout << "KeyCallable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
|
||||
// Check if we should exit on exception
|
||||
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
|
||||
McRFPy_API::signalPythonException();
|
||||
}
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "KeyCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
|
|
|
|||
|
|
@ -6,15 +6,24 @@ class PyCallable
|
|||
{
|
||||
protected:
|
||||
PyObject* target;
|
||||
|
||||
public:
|
||||
PyCallable(PyObject*);
|
||||
PyCallable(const PyCallable& other);
|
||||
PyCallable& operator=(const PyCallable& other);
|
||||
~PyCallable();
|
||||
PyObject* call(PyObject*, PyObject*);
|
||||
bool isNone() const;
|
||||
PyObject* borrow() const { return target; }
|
||||
public:
|
||||
bool isNone();
|
||||
};
|
||||
|
||||
class PyTimerCallable: public PyCallable
|
||||
{
|
||||
private:
|
||||
int interval;
|
||||
int last_ran;
|
||||
void call(int);
|
||||
public:
|
||||
bool hasElapsed(int);
|
||||
bool test(int);
|
||||
PyTimerCallable(PyObject*, int, int);
|
||||
PyTimerCallable();
|
||||
};
|
||||
|
||||
class PyClickCallable: public PyCallable
|
||||
|
|
@ -24,11 +33,6 @@ public:
|
|||
PyObject* borrow();
|
||||
PyClickCallable(PyObject*);
|
||||
PyClickCallable();
|
||||
PyClickCallable(const PyClickCallable& other) : PyCallable(other) {}
|
||||
PyClickCallable& operator=(const PyClickCallable& other) {
|
||||
PyCallable::operator=(other);
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
class PyKeyCallable: public PyCallable
|
||||
|
|
|
|||
149
src/PyColor.cpp
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,211 +0,0 @@
|
|||
#include "PyDrawable.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
// Click property getter
|
||||
static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure)
|
||||
{
|
||||
if (!self->data->click_callable)
|
||||
Py_RETURN_NONE;
|
||||
|
||||
PyObject* ptr = self->data->click_callable->borrow();
|
||||
if (ptr && ptr != Py_None)
|
||||
return ptr;
|
||||
else
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Click property setter
|
||||
static int PyDrawable_set_click(PyDrawableObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (value == Py_None) {
|
||||
self->data->click_unregister();
|
||||
} else if (PyCallable_Check(value)) {
|
||||
self->data->click_register(value);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "click must be callable or None");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Z-index property getter
|
||||
static PyObject* PyDrawable_get_z_index(PyDrawableObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->z_index);
|
||||
}
|
||||
|
||||
// Z-index property setter
|
||||
static int PyDrawable_set_z_index(PyDrawableObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!PyLong_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int val = PyLong_AsLong(value);
|
||||
self->data->z_index = val;
|
||||
|
||||
// Mark scene as needing resort
|
||||
self->data->notifyZIndexChanged();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Visible property getter (new for #87)
|
||||
static PyObject* PyDrawable_get_visible(PyDrawableObject* self, void* closure)
|
||||
{
|
||||
return PyBool_FromLong(self->data->visible);
|
||||
}
|
||||
|
||||
// Visible property setter (new for #87)
|
||||
static int PyDrawable_set_visible(PyDrawableObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!PyBool_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data->visible = (value == Py_True);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Opacity property getter (new for #88)
|
||||
static PyObject* PyDrawable_get_opacity(PyDrawableObject* self, void* closure)
|
||||
{
|
||||
return PyFloat_FromDouble(self->data->opacity);
|
||||
}
|
||||
|
||||
// Opacity property setter (new for #88)
|
||||
static int PyDrawable_set_opacity(PyDrawableObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
float val;
|
||||
if (PyFloat_Check(value)) {
|
||||
val = PyFloat_AsDouble(value);
|
||||
} else if (PyLong_Check(value)) {
|
||||
val = PyLong_AsLong(value);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Clamp to valid range
|
||||
if (val < 0.0f) val = 0.0f;
|
||||
if (val > 1.0f) val = 1.0f;
|
||||
|
||||
self->data->opacity = val;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// GetSetDef array for properties
|
||||
static PyGetSetDef PyDrawable_getsetters[] = {
|
||||
{"on_click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click,
|
||||
MCRF_PROPERTY(on_click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (x, y) coordinates of click."
|
||||
), NULL},
|
||||
{"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index,
|
||||
MCRF_PROPERTY(z_index,
|
||||
"Z-order for rendering (lower values rendered first). "
|
||||
"Automatically triggers scene resort when changed."
|
||||
), NULL},
|
||||
{"visible", (getter)PyDrawable_get_visible, (setter)PyDrawable_set_visible,
|
||||
MCRF_PROPERTY(visible,
|
||||
"Whether the object is visible (bool). "
|
||||
"Invisible objects are not rendered or clickable."
|
||||
), NULL},
|
||||
{"opacity", (getter)PyDrawable_get_opacity, (setter)PyDrawable_set_opacity,
|
||||
MCRF_PROPERTY(opacity,
|
||||
"Opacity level (0.0 = transparent, 1.0 = opaque). "
|
||||
"Automatically clamped to valid range [0.0, 1.0]."
|
||||
), NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
||||
// get_bounds method implementation (#89)
|
||||
static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUSED(args))
|
||||
{
|
||||
auto bounds = self->data->get_bounds();
|
||||
return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height);
|
||||
}
|
||||
|
||||
// move method implementation (#98)
|
||||
static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args)
|
||||
{
|
||||
float dx, dy;
|
||||
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->data->move(dx, dy);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// resize method implementation (#98)
|
||||
static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args)
|
||||
{
|
||||
float w, h;
|
||||
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
self->data->resize(w, h);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Method definitions
|
||||
static PyMethodDef PyDrawable_methods[] = {
|
||||
{"get_bounds", (PyCFunction)PyDrawable_get_bounds, METH_NOARGS,
|
||||
MCRF_METHOD(Drawable, get_bounds,
|
||||
MCRF_SIG("()", "tuple"),
|
||||
MCRF_DESC("Get the bounding rectangle of this drawable element."),
|
||||
MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds")
|
||||
MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.")
|
||||
)},
|
||||
{"move", (PyCFunction)PyDrawable_move, METH_VARARGS,
|
||||
MCRF_METHOD(Drawable, move,
|
||||
MCRF_SIG("(dx: float, dy: float)", "None"),
|
||||
MCRF_DESC("Move the element by a relative offset."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("dx", "Horizontal offset in pixels")
|
||||
MCRF_ARG("dy", "Vertical offset in pixels")
|
||||
MCRF_NOTE("This modifies the x and y position properties by the given amounts.")
|
||||
)},
|
||||
{"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS,
|
||||
MCRF_METHOD(Drawable, resize,
|
||||
MCRF_SIG("(width: float, height: float)", "None"),
|
||||
MCRF_DESC("Resize the element to new dimensions."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("width", "New width in pixels")
|
||||
MCRF_ARG("height", "New height in pixels")
|
||||
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.")
|
||||
)},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
||||
// Type initialization
|
||||
static int PyDrawable_init(PyDrawableObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
PyErr_SetString(PyExc_TypeError, "Drawable is an abstract base class and cannot be instantiated directly");
|
||||
return -1;
|
||||
}
|
||||
|
||||
namespace mcrfpydef {
|
||||
PyTypeObject PyDrawableType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Drawable",
|
||||
.tp_basicsize = sizeof(PyDrawableObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self) {
|
||||
PyDrawableObject* obj = (PyDrawableObject*)self;
|
||||
obj->data.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_doc = PyDoc_STR("Base class for all drawable UI elements"),
|
||||
.tp_methods = PyDrawable_methods,
|
||||
.tp_getset = PyDrawable_getsetters,
|
||||
.tp_init = (initproc)PyDrawable_init,
|
||||
.tp_new = PyType_GenericNew,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "UIDrawable.h"
|
||||
|
||||
// Python object structure for UIDrawable base class
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UIDrawable> data;
|
||||
} PyDrawableObject;
|
||||
|
||||
// Declare the Python type for Drawable base class
|
||||
namespace mcrfpydef {
|
||||
extern PyTypeObject PyDrawableType;
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
#include "PyFont.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
|
||||
PyFont::PyFont(std::string filename)
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
};
|
||||
160
src/PyScene.cpp
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,464 +0,0 @@
|
|||
#include "PySceneObject.h"
|
||||
#include "PyScene.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <iostream>
|
||||
|
||||
// Static map to store Python scene objects by name
|
||||
static std::map<std::string, PySceneObject*> python_scenes;
|
||||
|
||||
PyObject* PySceneClass::__new__(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
PySceneObject* self = (PySceneObject*)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
self->initialized = false;
|
||||
// Don't create C++ scene yet - wait for __init__
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
int PySceneClass::__init__(PySceneObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* keywords[] = {"name", nullptr};
|
||||
const char* name = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char**>(keywords), &name)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check if scene with this name already exists
|
||||
if (python_scenes.count(name) > 0) {
|
||||
PyErr_Format(PyExc_ValueError, "Scene with name '%s' already exists", name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->name = name;
|
||||
|
||||
// Create the C++ PyScene
|
||||
McRFPy_API::game->createScene(name);
|
||||
|
||||
// Get reference to the created scene
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Store this Python object in our registry
|
||||
python_scenes[name] = self;
|
||||
Py_INCREF(self); // Keep a reference
|
||||
|
||||
// Create a Python function that routes to on_keypress
|
||||
// We'll register this after the object is fully initialized
|
||||
|
||||
self->initialized = true;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PySceneClass::__dealloc(PyObject* self_obj)
|
||||
{
|
||||
PySceneObject* self = (PySceneObject*)self_obj;
|
||||
|
||||
// Remove from registry
|
||||
if (python_scenes.count(self->name) > 0 && python_scenes[self->name] == self) {
|
||||
python_scenes.erase(self->name);
|
||||
}
|
||||
|
||||
// Call Python object destructor
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
}
|
||||
|
||||
PyObject* PySceneClass::__repr__(PySceneObject* self)
|
||||
{
|
||||
return PyUnicode_FromFormat("<Scene '%s'>", self->name.c_str());
|
||||
}
|
||||
|
||||
PyObject* PySceneClass::activate(PySceneObject* self, PyObject* args)
|
||||
{
|
||||
// Call the static method from McRFPy_API
|
||||
PyObject* py_args = Py_BuildValue("(s)", self->name.c_str());
|
||||
PyObject* result = McRFPy_API::_setScene(NULL, py_args);
|
||||
Py_DECREF(py_args);
|
||||
return result;
|
||||
}
|
||||
|
||||
PyObject* PySceneClass::get_ui(PySceneObject* self, PyObject* args)
|
||||
{
|
||||
// Call the static method from McRFPy_API
|
||||
PyObject* py_args = Py_BuildValue("(s)", self->name.c_str());
|
||||
PyObject* result = McRFPy_API::_sceneUI(NULL, py_args);
|
||||
Py_DECREF(py_args);
|
||||
return result;
|
||||
}
|
||||
|
||||
PyObject* PySceneClass::register_keyboard(PySceneObject* self, PyObject* args)
|
||||
{
|
||||
PyObject* callable;
|
||||
if (!PyArg_ParseTuple(args, "O", &callable)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!PyCallable_Check(callable)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Argument must be callable");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Store the callable
|
||||
Py_INCREF(callable);
|
||||
|
||||
// Get the current scene and set its key_callable
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (game) {
|
||||
// We need to be on the right scene first
|
||||
std::string old_scene = game->scene;
|
||||
game->scene = self->name;
|
||||
game->currentScene()->key_callable = std::make_unique<PyKeyCallable>(callable);
|
||||
game->scene = old_scene;
|
||||
}
|
||||
|
||||
Py_DECREF(callable);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PySceneClass::get_name(PySceneObject* self, void* closure)
|
||||
{
|
||||
return PyUnicode_FromString(self->name.c_str());
|
||||
}
|
||||
|
||||
PyObject* PySceneClass::get_active(PySceneObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(game->scene == self->name);
|
||||
}
|
||||
|
||||
// #118: Scene position getter
|
||||
static PyObject* PySceneClass_get_pos(PySceneObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Get the scene by name using the public accessor
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Create a Vector object
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (!type) return NULL;
|
||||
PyObject* args = Py_BuildValue("(ff)", scene->position.x, scene->position.y);
|
||||
PyObject* result = PyObject_CallObject((PyObject*)type, args);
|
||||
Py_DECREF(type);
|
||||
Py_DECREF(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
// #118: Scene position setter
|
||||
static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine");
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Accept tuple or Vector
|
||||
float x, y;
|
||||
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
|
||||
x = PyFloat_AsDouble(PyTuple_GetItem(value, 0));
|
||||
y = PyFloat_AsDouble(PyTuple_GetItem(value, 1));
|
||||
} else if (PyObject_HasAttrString(value, "x") && PyObject_HasAttrString(value, "y")) {
|
||||
PyObject* xobj = PyObject_GetAttrString(value, "x");
|
||||
PyObject* yobj = PyObject_GetAttrString(value, "y");
|
||||
x = PyFloat_AsDouble(xobj);
|
||||
y = PyFloat_AsDouble(yobj);
|
||||
Py_DECREF(xobj);
|
||||
Py_DECREF(yobj);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
|
||||
scene->position = sf::Vector2f(x, y);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// #118: Scene visible getter
|
||||
static PyObject* PySceneClass_get_visible(PySceneObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(scene->visible);
|
||||
}
|
||||
|
||||
// #118: Scene visible setter
|
||||
static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine");
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyBool_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
scene->visible = PyObject_IsTrue(value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// #118: Scene opacity getter
|
||||
static PyObject* PySceneClass_get_opacity(PySceneObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
return PyFloat_FromDouble(1.0);
|
||||
}
|
||||
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
return PyFloat_FromDouble(1.0);
|
||||
}
|
||||
|
||||
return PyFloat_FromDouble(scene->opacity);
|
||||
}
|
||||
|
||||
// #118: Scene opacity setter
|
||||
static int PySceneClass_set_opacity(PySceneObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine");
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
double opacity;
|
||||
if (PyFloat_Check(value)) {
|
||||
opacity = PyFloat_AsDouble(value);
|
||||
} else if (PyLong_Check(value)) {
|
||||
opacity = PyLong_AsDouble(value);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Clamp to valid range
|
||||
if (opacity < 0.0) opacity = 0.0;
|
||||
if (opacity > 1.0) opacity = 1.0;
|
||||
|
||||
scene->opacity = opacity;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Lifecycle callbacks
|
||||
void PySceneClass::call_on_enter(PySceneObject* self)
|
||||
{
|
||||
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_enter");
|
||||
if (method && PyCallable_Check(method)) {
|
||||
PyObject* result = PyObject_CallNoArgs(method);
|
||||
if (result) {
|
||||
Py_DECREF(result);
|
||||
} else {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(method);
|
||||
} else {
|
||||
// Clear AttributeError if method doesn't exist
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
}
|
||||
|
||||
void PySceneClass::call_on_exit(PySceneObject* self)
|
||||
{
|
||||
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_exit");
|
||||
if (method && PyCallable_Check(method)) {
|
||||
PyObject* result = PyObject_CallNoArgs(method);
|
||||
if (result) {
|
||||
Py_DECREF(result);
|
||||
} else {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(method);
|
||||
} else {
|
||||
// Clear AttributeError if method doesn't exist
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
}
|
||||
|
||||
void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::string action)
|
||||
{
|
||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||
|
||||
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_keypress");
|
||||
if (method && PyCallable_Check(method)) {
|
||||
PyObject* result = PyObject_CallFunction(method, "ss", key.c_str(), action.c_str());
|
||||
if (result) {
|
||||
Py_DECREF(result);
|
||||
} else {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(method);
|
||||
} else {
|
||||
// Clear AttributeError if method doesn't exist
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
|
||||
PyGILState_Release(gstate);
|
||||
}
|
||||
|
||||
void PySceneClass::call_update(PySceneObject* self, float dt)
|
||||
{
|
||||
PyObject* method = PyObject_GetAttrString((PyObject*)self, "update");
|
||||
if (method && PyCallable_Check(method)) {
|
||||
PyObject* result = PyObject_CallFunction(method, "f", dt);
|
||||
if (result) {
|
||||
Py_DECREF(result);
|
||||
} else {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(method);
|
||||
} else {
|
||||
// Clear AttributeError if method doesn't exist
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
}
|
||||
|
||||
void PySceneClass::call_on_resize(PySceneObject* self, int width, int height)
|
||||
{
|
||||
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_resize");
|
||||
if (method && PyCallable_Check(method)) {
|
||||
PyObject* result = PyObject_CallFunction(method, "ii", width, height);
|
||||
if (result) {
|
||||
Py_DECREF(result);
|
||||
} else {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(method);
|
||||
} else {
|
||||
// Clear AttributeError if method doesn't exist
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
}
|
||||
|
||||
// Properties
|
||||
PyGetSetDef PySceneClass::getsetters[] = {
|
||||
{"name", (getter)get_name, NULL,
|
||||
MCRF_PROPERTY(name, "Scene name (str, read-only). Unique identifier for this scene."), NULL},
|
||||
{"active", (getter)get_active, NULL,
|
||||
MCRF_PROPERTY(active, "Whether this scene is currently active (bool, read-only). Only one scene can be active at a time."), NULL},
|
||||
// #118: Scene-level UIDrawable-like properties
|
||||
{"pos", (getter)PySceneClass_get_pos, (setter)PySceneClass_set_pos,
|
||||
MCRF_PROPERTY(pos, "Scene position offset (Vector). Applied to all UI elements during rendering."), NULL},
|
||||
{"visible", (getter)PySceneClass_get_visible, (setter)PySceneClass_set_visible,
|
||||
MCRF_PROPERTY(visible, "Scene visibility (bool). If False, scene is not rendered."), NULL},
|
||||
{"opacity", (getter)PySceneClass_get_opacity, (setter)PySceneClass_set_opacity,
|
||||
MCRF_PROPERTY(opacity, "Scene opacity (0.0-1.0). Applied to all UI elements during rendering."), NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
// Methods
|
||||
PyMethodDef PySceneClass::methods[] = {
|
||||
{"activate", (PyCFunction)activate, METH_NOARGS,
|
||||
MCRF_METHOD(SceneClass, activate,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Make this the active scene."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Deactivates the current scene and activates this one. Scene transitions and lifecycle callbacks are triggered.")
|
||||
)},
|
||||
{"get_ui", (PyCFunction)get_ui, METH_NOARGS,
|
||||
MCRF_METHOD(SceneClass, get_ui,
|
||||
MCRF_SIG("()", "UICollection"),
|
||||
MCRF_DESC("Get the UI element collection for this scene."),
|
||||
MCRF_RETURNS("UICollection: Collection of UI elements (Frames, Captions, Sprites, Grids) in this scene")
|
||||
MCRF_NOTE("Use to add, remove, or iterate over UI elements. Changes are reflected immediately.")
|
||||
)},
|
||||
{"register_keyboard", (PyCFunction)register_keyboard, METH_VARARGS,
|
||||
MCRF_METHOD(SceneClass, register_keyboard,
|
||||
MCRF_SIG("(callback: callable)", "None"),
|
||||
MCRF_DESC("Register a keyboard event handler function."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("callback", "Function that receives (key: str, pressed: bool) when keyboard events occur")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Alternative to overriding on_keypress() method. Handler is called for both key press and release events.")
|
||||
)},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
// Helper function to trigger lifecycle events
|
||||
void McRFPy_API::triggerSceneChange(const std::string& from_scene, const std::string& to_scene)
|
||||
{
|
||||
// Call on_exit for the old scene
|
||||
if (!from_scene.empty() && python_scenes.count(from_scene) > 0) {
|
||||
PySceneClass::call_on_exit(python_scenes[from_scene]);
|
||||
}
|
||||
|
||||
// Call on_enter for the new scene
|
||||
if (!to_scene.empty() && python_scenes.count(to_scene) > 0) {
|
||||
PySceneClass::call_on_enter(python_scenes[to_scene]);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to update Python scenes
|
||||
void McRFPy_API::updatePythonScenes(float dt)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) return;
|
||||
|
||||
// Only update the active scene
|
||||
if (python_scenes.count(game->scene) > 0) {
|
||||
PySceneClass::call_update(python_scenes[game->scene], dt);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to trigger resize events on Python scenes
|
||||
void McRFPy_API::triggerResize(int width, int height)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) return;
|
||||
|
||||
// Only notify the active scene
|
||||
if (python_scenes.count(game->scene) > 0) {
|
||||
PySceneClass::call_on_resize(python_scenes[game->scene], width, height);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
// Forward declarations
|
||||
class PyScene;
|
||||
|
||||
// Python object structure for Scene
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::string name;
|
||||
std::shared_ptr<PyScene> scene; // Reference to the C++ scene
|
||||
bool initialized;
|
||||
} PySceneObject;
|
||||
|
||||
// C++ interface for Python Scene class
|
||||
class PySceneClass
|
||||
{
|
||||
public:
|
||||
// Type methods
|
||||
static PyObject* __new__(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
||||
static int __init__(PySceneObject* self, PyObject* args, PyObject* kwds);
|
||||
static void __dealloc(PyObject* self);
|
||||
static PyObject* __repr__(PySceneObject* self);
|
||||
|
||||
// Scene methods
|
||||
static PyObject* activate(PySceneObject* self, PyObject* args);
|
||||
static PyObject* get_ui(PySceneObject* self, PyObject* args);
|
||||
static PyObject* register_keyboard(PySceneObject* self, PyObject* args);
|
||||
|
||||
// Properties
|
||||
static PyObject* get_name(PySceneObject* self, void* closure);
|
||||
static PyObject* get_active(PySceneObject* self, void* closure);
|
||||
|
||||
// Lifecycle callbacks (called from C++)
|
||||
static void call_on_enter(PySceneObject* self);
|
||||
static void call_on_exit(PySceneObject* self);
|
||||
static void call_on_keypress(PySceneObject* self, std::string key, std::string action);
|
||||
static void call_update(PySceneObject* self, float dt);
|
||||
static void call_on_resize(PySceneObject* self, int width, int height);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyMethodDef methods[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
static PyTypeObject PySceneType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Scene",
|
||||
.tp_basicsize = sizeof(PySceneObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)PySceneClass::__dealloc,
|
||||
.tp_repr = (reprfunc)PySceneClass::__repr__,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // Allow subclassing
|
||||
.tp_doc = PyDoc_STR("Base class for object-oriented scenes"),
|
||||
.tp_methods = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_getset = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_init = (initproc)PySceneClass::__init__,
|
||||
.tp_new = PySceneClass::__new__,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,61 +1,23 @@
|
|||
#include "PyTexture.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
|
||||
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0)
|
||||
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h)
|
||||
{
|
||||
texture = sf::Texture();
|
||||
if (!texture.loadFromFile(source)) {
|
||||
// Failed to load texture - leave sheet dimensions as 0
|
||||
// This will be checked in init()
|
||||
return;
|
||||
}
|
||||
texture.setSmooth(false); // Disable smoothing for pixel art
|
||||
texture.loadFromFile(source);
|
||||
auto size = texture.getSize();
|
||||
sheet_width = (size.x / sprite_width);
|
||||
sheet_height = (size.y / sprite_height);
|
||||
if (size.x % sprite_width != 0 || size.y % sprite_height != 0)
|
||||
{
|
||||
std::cout << "Warning: Texture `" << source << "` is not an even number of sprite widths or heights across." << std::endl
|
||||
std::cout << "Warning: Texture `" << source << "` is not an even number of sprite widths or heights across." << std::endl
|
||||
<< "Sprite size given was " << sprite_w << "x" << sprite_h << "px but the file has a resolution of " << sheet_width << "x" << sheet_height << "px." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// #144: Factory method to create texture from rendered content (snapshot)
|
||||
std::shared_ptr<PyTexture> PyTexture::from_rendered(sf::RenderTexture& render_tex)
|
||||
{
|
||||
// Use a custom shared_ptr construction to access private default constructor
|
||||
struct MakeSharedEnabler : public PyTexture {
|
||||
MakeSharedEnabler() : PyTexture() {}
|
||||
};
|
||||
auto ptex = std::make_shared<MakeSharedEnabler>();
|
||||
|
||||
// Copy the rendered texture data
|
||||
ptex->texture = render_tex.getTexture();
|
||||
ptex->texture.setSmooth(false); // Maintain pixel art aesthetic
|
||||
|
||||
// Set source to indicate this is a snapshot
|
||||
ptex->source = "<snapshot>";
|
||||
|
||||
// Treat entire texture as single sprite
|
||||
auto size = ptex->texture.getSize();
|
||||
ptex->sprite_width = size.x;
|
||||
ptex->sprite_height = size.y;
|
||||
ptex->sheet_width = 1;
|
||||
ptex->sheet_height = 1;
|
||||
|
||||
return ptex;
|
||||
}
|
||||
|
||||
sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s)
|
||||
{
|
||||
// Protect against division by zero if texture failed to load
|
||||
if (sheet_width == 0 || sheet_height == 0) {
|
||||
// Return an empty sprite
|
||||
return sf::Sprite();
|
||||
}
|
||||
|
||||
int tx = index % sheet_width, ty = index / sheet_width;
|
||||
auto ir = sf::IntRect(tx * sprite_width, ty * sprite_height, sprite_width, sprite_height);
|
||||
auto sprite = sf::Sprite(texture, ir);
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
357
src/PyTimer.cpp
|
|
@ -1,357 +0,0 @@
|
|||
#include "PyTimer.h"
|
||||
#include "Timer.h"
|
||||
#include "GameEngine.h"
|
||||
#include "Resources.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <sstream>
|
||||
|
||||
PyObject* PyTimer::repr(PyObject* self) {
|
||||
PyTimerObject* timer = (PyTimerObject*)self;
|
||||
std::ostringstream oss;
|
||||
oss << "<Timer name='" << timer->name << "' ";
|
||||
|
||||
if (timer->data) {
|
||||
oss << "interval=" << timer->data->getInterval() << "ms ";
|
||||
if (timer->data->isOnce()) {
|
||||
oss << "once=True ";
|
||||
}
|
||||
if (timer->data->isPaused()) {
|
||||
oss << "paused";
|
||||
// Get current time to show remaining
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
oss << " (remaining=" << timer->data->getRemaining(current_time) << "ms)";
|
||||
} else if (timer->data->isActive()) {
|
||||
oss << "active";
|
||||
} else {
|
||||
oss << "cancelled";
|
||||
}
|
||||
} else {
|
||||
oss << "uninitialized";
|
||||
}
|
||||
oss << ">";
|
||||
|
||||
return PyUnicode_FromString(oss.str().c_str());
|
||||
}
|
||||
|
||||
PyObject* PyTimer::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
|
||||
PyTimerObject* self = (PyTimerObject*)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
new(&self->name) std::string(); // Placement new for std::string
|
||||
self->data = nullptr;
|
||||
self->weakreflist = nullptr; // Initialize weakref list
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"name", "callback", "interval", "once", NULL};
|
||||
const char* name = nullptr;
|
||||
PyObject* callback = nullptr;
|
||||
int interval = 0;
|
||||
int once = 0; // Use int for bool parameter
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi|p", const_cast<char**>(kwlist),
|
||||
&name, &callback, &interval, &once)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyCallable_Check(callback)) {
|
||||
PyErr_SetString(PyExc_TypeError, "callback must be callable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (interval <= 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "interval must be positive");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->name = name;
|
||||
|
||||
// Get current time from game engine
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
// Create the timer
|
||||
self->data = std::make_shared<Timer>(callback, interval, current_time, (bool)once);
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
// Register with game engine
|
||||
if (Resources::game) {
|
||||
Resources::game->timers[self->name] = self->data;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PyTimer::dealloc(PyTimerObject* self) {
|
||||
// Clear weakrefs first
|
||||
if (self->weakreflist != nullptr) {
|
||||
PyObject_ClearWeakRefs((PyObject*)self);
|
||||
}
|
||||
|
||||
// Remove from game engine if still registered
|
||||
if (Resources::game && !self->name.empty()) {
|
||||
auto it = Resources::game->timers.find(self->name);
|
||||
if (it != Resources::game->timers.end() && it->second == self->data) {
|
||||
Resources::game->timers.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly destroy std::string
|
||||
self->name.~basic_string();
|
||||
|
||||
// Clear shared_ptr
|
||||
self->data.reset();
|
||||
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
// Timer control methods
|
||||
PyObject* PyTimer::pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
self->data->pause(current_time);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
self->data->resume(current_time);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Remove from game engine
|
||||
if (Resources::game && !self->name.empty()) {
|
||||
auto it = Resources::game->timers.find(self->name);
|
||||
if (it != Resources::game->timers.end() && it->second == self->data) {
|
||||
Resources::game->timers.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
self->data->cancel();
|
||||
self->data.reset();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
self->data->restart(current_time);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Property getters/setters
|
||||
PyObject* PyTimer::get_interval(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return PyLong_FromLong(self->data->getInterval());
|
||||
}
|
||||
|
||||
int PyTimer::set_interval(PyTimerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyLong_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "interval must be an integer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
long interval = PyLong_AsLong(value);
|
||||
if (interval <= 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "interval must be positive");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data->setInterval(interval);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_remaining(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
return PyLong_FromLong(self->data->getRemaining(current_time));
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_paused(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(self->data->isPaused());
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_active(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
return Py_False;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(self->data->isActive());
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_callback(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObject* callback = self->data->getCallback();
|
||||
if (!callback) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
Py_INCREF(callback);
|
||||
return callback;
|
||||
}
|
||||
|
||||
int PyTimer::set_callback(PyTimerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyCallable_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "callback must be callable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data->setCallback(value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_once(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(self->data->isOnce());
|
||||
}
|
||||
|
||||
int PyTimer::set_once(PyTimerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyBool_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "once must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data->setOnce(PyObject_IsTrue(value));
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_name(PyTimerObject* self, void* closure) {
|
||||
return PyUnicode_FromString(self->name.c_str());
|
||||
}
|
||||
|
||||
PyGetSetDef PyTimer::getsetters[] = {
|
||||
{"name", (getter)PyTimer::get_name, NULL,
|
||||
MCRF_PROPERTY(name, "Timer name (str, read-only). Unique identifier for this timer."), NULL},
|
||||
{"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval,
|
||||
MCRF_PROPERTY(interval, "Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running."), NULL},
|
||||
{"remaining", (getter)PyTimer::get_remaining, NULL,
|
||||
MCRF_PROPERTY(remaining, "Time remaining until next trigger in milliseconds (int, read-only). Preserved when timer is paused."), NULL},
|
||||
{"paused", (getter)PyTimer::get_paused, NULL,
|
||||
MCRF_PROPERTY(paused, "Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time."), NULL},
|
||||
{"active", (getter)PyTimer::get_active, NULL,
|
||||
MCRF_PROPERTY(active, "Whether the timer is active and not paused (bool, read-only). False if cancelled or paused."), NULL},
|
||||
{"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback,
|
||||
MCRF_PROPERTY(callback, "The callback function to be called when timer fires (callable). Can be changed while timer is running."), NULL},
|
||||
{"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once,
|
||||
MCRF_PROPERTY(once, "Whether the timer stops after firing once (bool). If False, timer repeats indefinitely."), NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyMethodDef PyTimer::methods[] = {
|
||||
{"pause", (PyCFunction)PyTimer::pause, METH_NOARGS,
|
||||
MCRF_METHOD(Timer, pause,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Pause the timer, preserving the time remaining until next trigger."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("The timer can be resumed later with resume(). Time spent paused does not count toward the interval.")
|
||||
)},
|
||||
{"resume", (PyCFunction)PyTimer::resume, METH_NOARGS,
|
||||
MCRF_METHOD(Timer, resume,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Resume a paused timer from where it left off."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Has no effect if the timer is not paused. Timer will fire after the remaining time elapses.")
|
||||
)},
|
||||
{"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS,
|
||||
MCRF_METHOD(Timer, cancel,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Cancel the timer and remove it from the timer system."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("The timer will no longer fire and cannot be restarted. The callback will not be called again.")
|
||||
)},
|
||||
{"restart", (PyCFunction)PyTimer::restart, METH_NOARGS,
|
||||
MCRF_METHOD(Timer, restart,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Restart the timer from the beginning."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Resets the timer to fire after a full interval from now, regardless of remaining time.")
|
||||
)},
|
||||
{NULL}
|
||||
};
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class Timer;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<Timer> data;
|
||||
std::string name;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyTimerObject;
|
||||
|
||||
class PyTimer
|
||||
{
|
||||
public:
|
||||
// Python type methods
|
||||
static PyObject* repr(PyObject* self);
|
||||
static int init(PyTimerObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
|
||||
static void dealloc(PyTimerObject* self);
|
||||
|
||||
// Timer control methods
|
||||
static PyObject* pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
|
||||
static PyObject* resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
|
||||
static PyObject* cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
|
||||
static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
|
||||
|
||||
// Timer property getters
|
||||
static PyObject* get_name(PyTimerObject* self, void* closure);
|
||||
static PyObject* get_interval(PyTimerObject* self, void* closure);
|
||||
static int set_interval(PyTimerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_remaining(PyTimerObject* self, void* closure);
|
||||
static PyObject* get_paused(PyTimerObject* self, void* closure);
|
||||
static PyObject* get_active(PyTimerObject* self, void* closure);
|
||||
static PyObject* get_callback(PyTimerObject* self, void* closure);
|
||||
static int set_callback(PyTimerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_once(PyTimerObject* self, void* closure);
|
||||
static int set_once(PyTimerObject* self, PyObject* value, void* closure);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyMethodDef methods[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
static PyTypeObject PyTimerType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Timer",
|
||||
.tp_basicsize = sizeof(PyTimerObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)PyTimer::dealloc,
|
||||
.tp_repr = PyTimer::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Timer(name, callback, interval, once=False)\n\n"
|
||||
"Create a timer that calls a function at regular intervals.\n\n"
|
||||
"Args:\n"
|
||||
" name (str): Unique identifier for the timer\n"
|
||||
" callback (callable): Function to call - receives (timer, runtime) args\n"
|
||||
" interval (int): Time between calls in milliseconds\n"
|
||||
" once (bool): If True, timer stops after first call. Default: False\n\n"
|
||||
"Attributes:\n"
|
||||
" interval (int): Time between calls in milliseconds\n"
|
||||
" remaining (int): Time until next call in milliseconds (read-only)\n"
|
||||
" paused (bool): Whether timer is paused (read-only)\n"
|
||||
" active (bool): Whether timer is active and not paused (read-only)\n"
|
||||
" callback (callable): The callback function\n"
|
||||
" once (bool): Whether timer stops after firing once\n\n"
|
||||
"Methods:\n"
|
||||
" pause(): Pause the timer, preserving time remaining\n"
|
||||
" resume(): Resume a paused timer\n"
|
||||
" cancel(): Stop and remove the timer\n"
|
||||
" restart(): Reset timer to start from beginning\n\n"
|
||||
"Example:\n"
|
||||
" def on_timer(timer, runtime):\n"
|
||||
" print(f'Timer {timer} fired at {runtime}ms')\n"
|
||||
" if runtime > 5000:\n"
|
||||
" timer.cancel()\n"
|
||||
" \n"
|
||||
" timer = mcrfpy.Timer('my_timer', on_timer, 1000)\n"
|
||||
" timer.pause() # Pause timer\n"
|
||||
" timer.resume() # Resume timer\n"
|
||||
" timer.once = True # Make it one-shot"),
|
||||
.tp_methods = PyTimer::methods,
|
||||
.tp_getset = PyTimer::getsetters,
|
||||
.tp_init = (initproc)PyTimer::init,
|
||||
.tp_new = PyTimer::pynew,
|
||||
};
|
||||
}
|
||||
481
src/PyVector.cpp
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
532
src/PyWindow.cpp
|
|
@ -1,532 +0,0 @@
|
|||
#include "PyWindow.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <cstring>
|
||||
|
||||
// Singleton instance - static variable, not a class member
|
||||
static PyWindowObject* window_instance = nullptr;
|
||||
|
||||
PyObject* PyWindow::get(PyObject* cls, PyObject* args)
|
||||
{
|
||||
// Create singleton instance if it doesn't exist
|
||||
if (!window_instance) {
|
||||
// Use the class object passed as first argument
|
||||
PyTypeObject* type = (PyTypeObject*)cls;
|
||||
|
||||
if (!type->tp_alloc) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Window type not properly initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
window_instance = (PyWindowObject*)type->tp_alloc(type, 0);
|
||||
if (!window_instance) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
Py_INCREF(window_instance);
|
||||
return (PyObject*)window_instance;
|
||||
}
|
||||
|
||||
PyObject* PyWindow::repr(PyWindowObject* self)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
return PyUnicode_FromString("<Window [no game engine]>");
|
||||
}
|
||||
|
||||
if (game->isHeadless()) {
|
||||
return PyUnicode_FromString("<Window [headless mode]>");
|
||||
}
|
||||
|
||||
auto& window = game->getWindow();
|
||||
auto size = window.getSize();
|
||||
|
||||
return PyUnicode_FromFormat("<Window %dx%d>", size.x, size.y);
|
||||
}
|
||||
|
||||
// Property getters and setters
|
||||
|
||||
PyObject* PyWindow::get_resolution(PyWindowObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (game->isHeadless()) {
|
||||
// Return headless renderer size
|
||||
return Py_BuildValue("(ii)", 1024, 768); // Default headless size
|
||||
}
|
||||
|
||||
auto& window = game->getWindow();
|
||||
auto size = window.getSize();
|
||||
return Py_BuildValue("(ii)", size.x, size.y);
|
||||
}
|
||||
|
||||
int PyWindow::set_resolution(PyWindowObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (game->isHeadless()) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Cannot change resolution in headless mode");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int width, height;
|
||||
if (!PyArg_ParseTuple(value, "ii", &width, &height)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Resolution must be a tuple of two integers (width, height)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "Resolution dimensions must be positive");
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto& window = game->getWindow();
|
||||
|
||||
// Get current window settings
|
||||
auto style = sf::Style::Titlebar | sf::Style::Close;
|
||||
if (window.getSize() == sf::Vector2u(sf::VideoMode::getDesktopMode().width,
|
||||
sf::VideoMode::getDesktopMode().height)) {
|
||||
style = sf::Style::Fullscreen;
|
||||
}
|
||||
|
||||
// Recreate window with new size
|
||||
window.create(sf::VideoMode(width, height), game->getWindowTitle(), style);
|
||||
|
||||
// Restore vsync and framerate settings
|
||||
// Note: We'll need to store these settings in GameEngine
|
||||
window.setFramerateLimit(60); // Default for now
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyWindow::get_fullscreen(PyWindowObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (game->isHeadless()) {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
auto& window = game->getWindow();
|
||||
auto size = window.getSize();
|
||||
auto desktop = sf::VideoMode::getDesktopMode();
|
||||
|
||||
// Check if window size matches desktop size (rough fullscreen check)
|
||||
bool fullscreen = (size.x == desktop.width && size.y == desktop.height);
|
||||
|
||||
return PyBool_FromLong(fullscreen);
|
||||
}
|
||||
|
||||
int PyWindow::set_fullscreen(PyWindowObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (game->isHeadless()) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Cannot change fullscreen in headless mode");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyBool_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Fullscreen must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool fullscreen = PyObject_IsTrue(value);
|
||||
auto& window = game->getWindow();
|
||||
|
||||
if (fullscreen) {
|
||||
// Switch to fullscreen
|
||||
auto desktop = sf::VideoMode::getDesktopMode();
|
||||
window.create(desktop, game->getWindowTitle(), sf::Style::Fullscreen);
|
||||
} else {
|
||||
// Switch to windowed mode
|
||||
window.create(sf::VideoMode(1024, 768), game->getWindowTitle(),
|
||||
sf::Style::Titlebar | sf::Style::Close);
|
||||
}
|
||||
|
||||
// Restore settings
|
||||
window.setFramerateLimit(60);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyWindow::get_vsync(PyWindowObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(game->getVSync());
|
||||
}
|
||||
|
||||
int PyWindow::set_vsync(PyWindowObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (game->isHeadless()) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Cannot change vsync in headless mode");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyBool_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "vsync must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool vsync = PyObject_IsTrue(value);
|
||||
game->setVSync(vsync);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyWindow::get_title(PyWindowObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PyUnicode_FromString(game->getWindowTitle().c_str());
|
||||
}
|
||||
|
||||
int PyWindow::set_title(PyWindowObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (game->isHeadless()) {
|
||||
// Silently ignore in headless mode
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* title = PyUnicode_AsUTF8(value);
|
||||
if (!title) {
|
||||
PyErr_SetString(PyExc_TypeError, "Title must be a string");
|
||||
return -1;
|
||||
}
|
||||
|
||||
game->setWindowTitle(title);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyWindow::get_visible(PyWindowObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (game->isHeadless()) {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
auto& window = game->getWindow();
|
||||
bool visible = window.isOpen(); // Best approximation
|
||||
|
||||
return PyBool_FromLong(visible);
|
||||
}
|
||||
|
||||
int PyWindow::set_visible(PyWindowObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (game->isHeadless()) {
|
||||
// Silently ignore in headless mode
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!PyBool_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool visible = PyObject_IsTrue(value);
|
||||
auto& window = game->getWindow();
|
||||
window.setVisible(visible);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyWindow::get_framerate_limit(PyWindowObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PyLong_FromLong(game->getFramerateLimit());
|
||||
}
|
||||
|
||||
int PyWindow::set_framerate_limit(PyWindowObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (game->isHeadless()) {
|
||||
// Silently ignore in headless mode
|
||||
return 0;
|
||||
}
|
||||
|
||||
long limit = PyLong_AsLong(value);
|
||||
if (PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_TypeError, "framerate_limit must be an integer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (limit < 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "framerate_limit must be non-negative (0 for unlimited)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
game->setFramerateLimit(limit);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
PyObject* PyWindow::center(PyWindowObject* self, PyObject* args)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (game->isHeadless()) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Cannot center window in headless mode");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto& window = game->getWindow();
|
||||
auto size = window.getSize();
|
||||
auto desktop = sf::VideoMode::getDesktopMode();
|
||||
|
||||
int x = (desktop.width - size.x) / 2;
|
||||
int y = (desktop.height - size.y) / 2;
|
||||
|
||||
window.setPosition(sf::Vector2i(x, y));
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyWindow::screenshot(PyWindowObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* keywords[] = {"filename", NULL};
|
||||
const char* filename = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|s", const_cast<char**>(keywords), &filename)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the render target pointer
|
||||
sf::RenderTarget* target = game->getRenderTargetPtr();
|
||||
if (!target) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No render target available");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sf::Image screenshot;
|
||||
|
||||
// For RenderWindow
|
||||
if (auto* window = dynamic_cast<sf::RenderWindow*>(target)) {
|
||||
sf::Vector2u windowSize = window->getSize();
|
||||
sf::Texture texture;
|
||||
texture.create(windowSize.x, windowSize.y);
|
||||
texture.update(*window);
|
||||
screenshot = texture.copyToImage();
|
||||
}
|
||||
// For RenderTexture (headless mode)
|
||||
else if (auto* renderTexture = dynamic_cast<sf::RenderTexture*>(target)) {
|
||||
screenshot = renderTexture->getTexture().copyToImage();
|
||||
}
|
||||
else {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Unknown render target type");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Save to file if filename provided
|
||||
if (filename) {
|
||||
if (!screenshot.saveToFile(filename)) {
|
||||
PyErr_SetString(PyExc_IOError, "Failed to save screenshot");
|
||||
return NULL;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Otherwise return as bytes
|
||||
auto pixels = screenshot.getPixelsPtr();
|
||||
auto size = screenshot.getSize();
|
||||
|
||||
return PyBytes_FromStringAndSize((const char*)pixels, size.x * size.y * 4);
|
||||
}
|
||||
|
||||
PyObject* PyWindow::get_game_resolution(PyWindowObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto resolution = game->getGameResolution();
|
||||
return Py_BuildValue("(ii)", resolution.x, resolution.y);
|
||||
}
|
||||
|
||||
int PyWindow::set_game_resolution(PyWindowObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int width, height;
|
||||
if (!PyArg_ParseTuple(value, "ii", &width, &height)) {
|
||||
PyErr_SetString(PyExc_TypeError, "game_resolution must be a tuple of two integers (width, height)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "Game resolution dimensions must be positive");
|
||||
return -1;
|
||||
}
|
||||
|
||||
game->setGameResolution(width, height);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyWindow::get_scaling_mode(PyWindowObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PyUnicode_FromString(game->getViewportModeString().c_str());
|
||||
}
|
||||
|
||||
int PyWindow::set_scaling_mode(PyWindowObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* mode_str = PyUnicode_AsUTF8(value);
|
||||
if (!mode_str) {
|
||||
PyErr_SetString(PyExc_TypeError, "scaling_mode must be a string");
|
||||
return -1;
|
||||
}
|
||||
|
||||
GameEngine::ViewportMode mode;
|
||||
if (strcmp(mode_str, "center") == 0) {
|
||||
mode = GameEngine::ViewportMode::Center;
|
||||
} else if (strcmp(mode_str, "stretch") == 0) {
|
||||
mode = GameEngine::ViewportMode::Stretch;
|
||||
} else if (strcmp(mode_str, "fit") == 0) {
|
||||
mode = GameEngine::ViewportMode::Fit;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_ValueError, "scaling_mode must be 'center', 'stretch', or 'fit'");
|
||||
return -1;
|
||||
}
|
||||
|
||||
game->setViewportMode(mode);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Property definitions
|
||||
PyGetSetDef PyWindow::getsetters[] = {
|
||||
{"resolution", (getter)get_resolution, (setter)set_resolution,
|
||||
MCRF_PROPERTY(resolution, "Window resolution as (width, height) tuple. Setting this recreates the window."), NULL},
|
||||
{"fullscreen", (getter)get_fullscreen, (setter)set_fullscreen,
|
||||
MCRF_PROPERTY(fullscreen, "Window fullscreen state (bool). Setting this recreates the window."), NULL},
|
||||
{"vsync", (getter)get_vsync, (setter)set_vsync,
|
||||
MCRF_PROPERTY(vsync, "Vertical sync enabled state (bool). Prevents screen tearing but may limit framerate."), NULL},
|
||||
{"title", (getter)get_title, (setter)set_title,
|
||||
MCRF_PROPERTY(title, "Window title string (str). Displayed in the window title bar."), NULL},
|
||||
{"visible", (getter)get_visible, (setter)set_visible,
|
||||
MCRF_PROPERTY(visible, "Window visibility state (bool). Hidden windows still process events."), NULL},
|
||||
{"framerate_limit", (getter)get_framerate_limit, (setter)set_framerate_limit,
|
||||
MCRF_PROPERTY(framerate_limit, "Frame rate limit in FPS (int, 0 for unlimited). Caps maximum frame rate."), NULL},
|
||||
{"game_resolution", (getter)get_game_resolution, (setter)set_game_resolution,
|
||||
MCRF_PROPERTY(game_resolution, "Fixed game resolution as (width, height) tuple. Enables resolution-independent rendering with scaling."), NULL},
|
||||
{"scaling_mode", (getter)get_scaling_mode, (setter)set_scaling_mode,
|
||||
MCRF_PROPERTY(scaling_mode, "Viewport scaling mode (str): 'center' (no scaling), 'stretch' (fill window), or 'fit' (maintain aspect ratio)."), NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
// Method definitions
|
||||
PyMethodDef PyWindow::methods[] = {
|
||||
{"get", (PyCFunction)PyWindow::get, METH_VARARGS | METH_CLASS,
|
||||
MCRF_METHOD(Window, get,
|
||||
MCRF_SIG("()", "Window"),
|
||||
MCRF_DESC("Get the Window singleton instance."),
|
||||
MCRF_RETURNS("Window: The global window object")
|
||||
MCRF_NOTE("This is a class method. Call as Window.get(). There is only one window instance per application.")
|
||||
)},
|
||||
{"center", (PyCFunction)PyWindow::center, METH_NOARGS,
|
||||
MCRF_METHOD(Window, center,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Center the window on the screen."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Only works in windowed mode. Has no effect when fullscreen or in headless mode.")
|
||||
)},
|
||||
{"screenshot", (PyCFunction)PyWindow::screenshot, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Window, screenshot,
|
||||
MCRF_SIG("(filename: str = None)", "bytes | None"),
|
||||
MCRF_DESC("Take a screenshot of the current window contents."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("filename", "Optional path to save screenshot. If omitted, returns raw RGBA bytes.")
|
||||
MCRF_RETURNS("bytes | None: Raw RGBA pixel data if no filename given, otherwise None after saving")
|
||||
MCRF_NOTE("Screenshot is taken at the actual window resolution. Use after render loop update for current frame.")
|
||||
)},
|
||||
{NULL}
|
||||
};
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
|
||||
// Forward declarations
|
||||
class GameEngine;
|
||||
|
||||
// Python object structure for Window singleton
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
// No data - Window is a singleton that accesses GameEngine
|
||||
} PyWindowObject;
|
||||
|
||||
// C++ interface for the Window singleton
|
||||
class PyWindow
|
||||
{
|
||||
public:
|
||||
// Static methods for Python type
|
||||
static PyObject* get(PyObject* cls, PyObject* args);
|
||||
static PyObject* repr(PyWindowObject* self);
|
||||
|
||||
// Getters and setters for window properties
|
||||
static PyObject* get_resolution(PyWindowObject* self, void* closure);
|
||||
static int set_resolution(PyWindowObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_fullscreen(PyWindowObject* self, void* closure);
|
||||
static int set_fullscreen(PyWindowObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_vsync(PyWindowObject* self, void* closure);
|
||||
static int set_vsync(PyWindowObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_title(PyWindowObject* self, void* closure);
|
||||
static int set_title(PyWindowObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_visible(PyWindowObject* self, void* closure);
|
||||
static int set_visible(PyWindowObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_framerate_limit(PyWindowObject* self, void* closure);
|
||||
static int set_framerate_limit(PyWindowObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_game_resolution(PyWindowObject* self, void* closure);
|
||||
static int set_game_resolution(PyWindowObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_scaling_mode(PyWindowObject* self, void* closure);
|
||||
static int set_scaling_mode(PyWindowObject* self, PyObject* value, void* closure);
|
||||
|
||||
// Methods
|
||||
static PyObject* center(PyWindowObject* self, PyObject* args);
|
||||
static PyObject* screenshot(PyWindowObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyMethodDef methods[];
|
||||
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
static PyTypeObject PyWindowType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Window",
|
||||
.tp_basicsize = sizeof(PyWindowObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self) {
|
||||
// Don't delete the singleton instance
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)PyWindow::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Window singleton for accessing and modifying the game window properties"),
|
||||
.tp_methods = nullptr, // Set in McRFPy_API.cpp after definition
|
||||
.tp_getset = nullptr, // Set in McRFPy_API.cpp after definition
|
||||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
|
||||
PyErr_SetString(PyExc_TypeError, "Cannot instantiate Window. Use Window.get() to access the singleton.");
|
||||
return NULL;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
#include "PythonObjectCache.h"
|
||||
#include <iostream>
|
||||
|
||||
PythonObjectCache* PythonObjectCache::instance = nullptr;
|
||||
|
||||
PythonObjectCache& PythonObjectCache::getInstance() {
|
||||
if (!instance) {
|
||||
instance = new PythonObjectCache();
|
||||
}
|
||||
return *instance;
|
||||
}
|
||||
|
||||
PythonObjectCache::~PythonObjectCache() {
|
||||
clear();
|
||||
}
|
||||
|
||||
uint64_t PythonObjectCache::assignSerial() {
|
||||
return next_serial.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void PythonObjectCache::registerObject(uint64_t serial, PyObject* weakref) {
|
||||
if (!weakref || serial == 0) return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
|
||||
// Clean up any existing entry
|
||||
auto it = cache.find(serial);
|
||||
if (it != cache.end()) {
|
||||
Py_DECREF(it->second);
|
||||
}
|
||||
|
||||
// Store the new weak reference
|
||||
Py_INCREF(weakref);
|
||||
cache[serial] = weakref;
|
||||
}
|
||||
|
||||
PyObject* PythonObjectCache::lookup(uint64_t serial) {
|
||||
if (serial == 0) return nullptr;
|
||||
|
||||
// No mutex needed for read - GIL protects PyWeakref_GetObject
|
||||
auto it = cache.find(serial);
|
||||
if (it != cache.end()) {
|
||||
PyObject* obj = PyWeakref_GetObject(it->second);
|
||||
if (obj && obj != Py_None) {
|
||||
Py_INCREF(obj);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void PythonObjectCache::remove(uint64_t serial) {
|
||||
if (serial == 0) return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
auto it = cache.find(serial);
|
||||
if (it != cache.end()) {
|
||||
Py_DECREF(it->second);
|
||||
cache.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void PythonObjectCache::cleanup() {
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
|
||||
auto it = cache.begin();
|
||||
while (it != cache.end()) {
|
||||
PyObject* obj = PyWeakref_GetObject(it->second);
|
||||
if (!obj || obj == Py_None) {
|
||||
Py_DECREF(it->second);
|
||||
it = cache.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PythonObjectCache::clear() {
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
|
||||
for (auto& pair : cache) {
|
||||
Py_DECREF(pair.second);
|
||||
}
|
||||
cache.clear();
|
||||
}
|
||||