Implement --exec flag and PyAutoGUI-compatible automation API
- Add --exec flag to execute multiple scripts before main program - Scripts are executed in order and share Python interpreter state - Implement full PyAutoGUI-compatible automation API in McRFPy_Automation - Add screenshot, mouse control, keyboard input capabilities - Fix Python initialization issues when multiple scripts are loaded - Update CommandLineParser to handle --exec with proper sys.argv management - Add comprehensive examples and documentation This enables automation testing by allowing test scripts to run alongside games using the same Python environment. The automation API provides event injection into the SFML render loop for UI testing. Closes #32 partially (Python interpreter emulation) References automation testing requirements
This commit is contained in:
parent
763fa201f0
commit
68c1a016b0
|
@ -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")
|
|
@ -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
|
|
@ -11,10 +11,10 @@ public:
|
|||
const static int WHEEL_NUM = 4;
|
||||
const static int WHEEL_NEG = 2;
|
||||
const static int WHEEL_DEL = 1;
|
||||
static int keycode(sf::Keyboard::Key& k) { return KEY + (int)k; }
|
||||
static int keycode(sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
|
||||
static int keycode(const sf::Keyboard::Key& k) { return KEY + (int)k; }
|
||||
static int keycode(const sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
|
||||
//static int keycode(sf::Mouse::Wheel& w, float d) { return MOUSEWHEEL + (((int)w)<<12) + int(d*16) + 512; }
|
||||
static int keycode(sf::Mouse::Wheel& w, float d) {
|
||||
static int keycode(const sf::Mouse::Wheel& w, float d) {
|
||||
int neg = 0;
|
||||
if (d < 0) { neg = 1; }
|
||||
return MOUSEWHEEL + (w * WHEEL_NUM) + (neg * WHEEL_NEG) + 1;
|
||||
|
@ -32,7 +32,7 @@ public:
|
|||
return (a & WHEEL_DEL) * factor;
|
||||
}
|
||||
|
||||
static std::string key_str(sf::Keyboard::Key& keycode)
|
||||
static std::string key_str(const sf::Keyboard::Key& keycode)
|
||||
{
|
||||
switch(keycode)
|
||||
{
|
||||
|
|
|
@ -108,6 +108,20 @@ CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& confi
|
|||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--exec") {
|
||||
current_arg++;
|
||||
if (current_arg >= argc) {
|
||||
std::cerr << "Argument expected for the --exec option" << std::endl;
|
||||
result.should_exit = true;
|
||||
result.exit_code = 1;
|
||||
return result;
|
||||
}
|
||||
config.exec_scripts.push_back(argv[current_arg]);
|
||||
config.python_mode = true;
|
||||
current_arg++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If no flags matched, treat as positional argument (script name)
|
||||
if (arg[0] != '-') {
|
||||
config.script_path = arg;
|
||||
|
@ -141,6 +155,7 @@ void CommandLineParser::print_help() {
|
|||
<< " -V : print the Python version number and exit (also --version)\n"
|
||||
<< "\n"
|
||||
<< "McRogueFace specific options:\n"
|
||||
<< " --exec file : execute script before main program (can be used multiple times)\n"
|
||||
<< " --headless : run without creating a window (implies --audio-off)\n"
|
||||
<< " --audio-off : disable audio\n"
|
||||
<< " --audio-on : enable audio (even in headless mode)\n"
|
||||
|
|
|
@ -35,9 +35,35 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
|||
scenes["uitest"] = new UITestScene(this);
|
||||
|
||||
McRFPy_API::game = this;
|
||||
McRFPy_API::api_init();
|
||||
McRFPy_API::executePyString("import mcrfpy");
|
||||
McRFPy_API::executeScript("scripts/game.py");
|
||||
|
||||
// Only load game.py if no custom script/command/module/exec is specified
|
||||
bool should_load_game = config.script_path.empty() &&
|
||||
config.python_command.empty() &&
|
||||
config.python_module.empty() &&
|
||||
config.exec_scripts.empty() &&
|
||||
!config.interactive_mode &&
|
||||
!config.python_mode;
|
||||
|
||||
if (should_load_game) {
|
||||
if (!Py_IsInitialized()) {
|
||||
McRFPy_API::api_init();
|
||||
}
|
||||
McRFPy_API::executePyString("import mcrfpy");
|
||||
McRFPy_API::executeScript("scripts/game.py");
|
||||
}
|
||||
|
||||
// Execute any --exec scripts in order
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
clock.restart();
|
||||
runtime.restart();
|
||||
|
@ -166,86 +192,53 @@ void GameEngine::testTimers()
|
|||
}
|
||||
}
|
||||
|
||||
void GameEngine::processEvent(const sf::Event& event)
|
||||
{
|
||||
std::string actionType;
|
||||
int actionCode = 0;
|
||||
|
||||
if (event.type == sf::Event::Closed) { running = false; return; }
|
||||
// TODO: add resize event to Scene to react; call it after constructor too, maybe
|
||||
else if (event.type == sf::Event::Resized) {
|
||||
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";
|
||||
else if (event.type == sf::Event::KeyReleased || event.type == sf::Event::MouseButtonReleased) actionType = "end";
|
||||
|
||||
if (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseButtonReleased)
|
||||
actionCode = ActionCode::keycode(event.mouseButton.button);
|
||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
|
||||
actionCode = ActionCode::keycode(event.key.code);
|
||||
else if (event.type == sf::Event::MouseWheelScrolled)
|
||||
{
|
||||
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
|
||||
{
|
||||
int delta = 1;
|
||||
if (event.mouseWheelScroll.delta < 0) delta = -1;
|
||||
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
|
||||
}
|
||||
}
|
||||
else
|
||||
return;
|
||||
|
||||
if (currentScene()->hasAction(actionCode))
|
||||
{
|
||||
std::string name = currentScene()->action(actionCode);
|
||||
currentScene()->doAction(name, actionType);
|
||||
}
|
||||
else if (currentScene()->key_callable)
|
||||
{
|
||||
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
|
||||
}
|
||||
}
|
||||
|
||||
void GameEngine::sUserInput()
|
||||
{
|
||||
sf::Event event;
|
||||
while (window && window->pollEvent(event))
|
||||
{
|
||||
std::string actionType;
|
||||
int actionCode = 0;
|
||||
|
||||
if (event.type == sf::Event::Closed) { running = false; continue; }
|
||||
// TODO: add resize event to Scene to react; call it after constructor too, maybe
|
||||
else if (event.type == sf::Event::Resized) {
|
||||
continue; // 7DRL short circuit. Resizing manually disabled
|
||||
/*
|
||||
sf::FloatRect area(0.f, 0.f, event.size.width, event.size.height);
|
||||
//sf::FloatRect area(0.f, 0.f, 1024.f, 768.f); // 7DRL 2024: attempt to set scale appropriately
|
||||
//sf::FloatRect area(0.f, 0.f, event.size.width, event.size.width * 0.75);
|
||||
visible = sf::View(area);
|
||||
window.setView(visible);
|
||||
//window.setSize(sf::Vector2u(event.size.width, event.size.width * 0.75)); // 7DRL 2024: window scaling
|
||||
std::cout << "Visible area set to (0, 0, " << event.size.width << ", " << event.size.height <<")"<<std::endl;
|
||||
actionType = "resize";
|
||||
//window.setSize(sf::Vector2u(event.size.width, event.size.width * 0.75)); // 7DRL 2024: window scaling
|
||||
*/
|
||||
}
|
||||
|
||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start";
|
||||
else if (event.type == sf::Event::KeyReleased || event.type == sf::Event::MouseButtonReleased) actionType = "end";
|
||||
|
||||
if (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseButtonReleased)
|
||||
actionCode = ActionCode::keycode(event.mouseButton.button);
|
||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
|
||||
actionCode = ActionCode::keycode(event.key.code);
|
||||
else if (event.type == sf::Event::MouseWheelScrolled)
|
||||
{
|
||||
// //sf::Mouse::Wheel w = event.MouseWheelScrollEvent.wheel;
|
||||
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
|
||||
{
|
||||
int delta = 1;
|
||||
if (event.mouseWheelScroll.delta < 0) delta = -1;
|
||||
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
|
||||
/*
|
||||
std::cout << "[GameEngine] Generated MouseWheel code w(" << (int)event.mouseWheelScroll.wheel << ") d(" << event.mouseWheelScroll.delta << ") D(" << delta << ") = " << actionCode << std::endl;
|
||||
std::cout << " test decode: isMouseWheel=" << ActionCode::isMouseWheel(actionCode) << ", wheel=" << ActionCode::wheel(actionCode) << ", delta=" << ActionCode::delta(actionCode) << std::endl;
|
||||
std::cout << " math test: actionCode && WHEEL_NEG -> " << (actionCode && ActionCode::WHEEL_NEG) << "; actionCode && WHEEL_DEL -> " << (actionCode && ActionCode::WHEEL_DEL) << ";" << std::endl;
|
||||
*/
|
||||
}
|
||||
// float d = event.MouseWheelScrollEvent.delta;
|
||||
// actionCode = ActionCode::keycode(0, d);
|
||||
}
|
||||
else
|
||||
continue;
|
||||
|
||||
//std::cout << "Event produced action code " << actionCode << ": " << actionType << std::endl;
|
||||
|
||||
if (currentScene()->hasAction(actionCode))
|
||||
{
|
||||
std::string name = currentScene()->action(actionCode);
|
||||
currentScene()->doAction(name, actionType);
|
||||
}
|
||||
else if (currentScene()->key_callable)
|
||||
{
|
||||
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
|
||||
/*
|
||||
PyObject* args = Py_BuildValue("(ss)", ActionCode::key_str(event.key.code).c_str(), actionType.c_str());
|
||||
PyObject* retval = PyObject_Call(currentScene()->key_callable, args, NULL);
|
||||
if (!retval)
|
||||
{
|
||||
std::cout << "key_callable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "key_callable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
}
|
||||
*/
|
||||
}
|
||||
else
|
||||
{
|
||||
//std::cout << "[GameEngine] Action not registered for input: " << actionCode << ": " << actionType << std::endl;
|
||||
}
|
||||
processEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ public:
|
|||
sf::Font & getFont();
|
||||
sf::RenderWindow & getWindow();
|
||||
sf::RenderTarget & getRenderTarget();
|
||||
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
|
||||
void run();
|
||||
void sUserInput();
|
||||
int getFrame() { return currentFrame; }
|
||||
|
@ -55,6 +56,7 @@ public:
|
|||
void manageTimer(std::string, PyObject*, int);
|
||||
void setWindowScale(float);
|
||||
bool isHeadless() const { return headless; }
|
||||
void processEvent(const sf::Event& event);
|
||||
|
||||
// global textures for scripts to access
|
||||
std::vector<IndexTexture> textures;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Automation.h"
|
||||
#include "platform.h"
|
||||
#include "GameEngine.h"
|
||||
#include "UI.h"
|
||||
|
@ -104,6 +105,17 @@ PyObject* PyInit_mcrfpy()
|
|||
//PyModule_AddObject(m, "default_texture", McRFPy_API::default_texture->pyObject());
|
||||
PyModule_AddObject(m, "default_font", Py_None);
|
||||
PyModule_AddObject(m, "default_texture", Py_None);
|
||||
|
||||
// Add automation submodule
|
||||
PyObject* automation_module = McRFPy_Automation::init_automation_module();
|
||||
if (automation_module != NULL) {
|
||||
PyModule_AddObject(m, "automation", automation_module);
|
||||
|
||||
// Also add to sys.modules for proper import behavior
|
||||
PyObject* sys_modules = PyImport_GetModuleDict();
|
||||
PyDict_SetItemString(sys_modules, "mcrfpy.automation", automation_module);
|
||||
}
|
||||
|
||||
//McRFPy_API::mcrf_module = m;
|
||||
return m;
|
||||
}
|
||||
|
@ -164,6 +176,11 @@ PyStatus init_python(const char *program_name)
|
|||
|
||||
PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv)
|
||||
{
|
||||
// If Python is already initialized, just return success
|
||||
if (Py_IsInitialized()) {
|
||||
return PyStatus_Ok();
|
||||
}
|
||||
|
||||
PyStatus status;
|
||||
PyConfig pyconfig;
|
||||
PyConfig_InitIsolatedConfig(&pyconfig);
|
||||
|
@ -216,7 +233,9 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, in
|
|||
#endif
|
||||
|
||||
// Register mcrfpy module before initialization
|
||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||
if (!Py_IsInitialized()) {
|
||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||
}
|
||||
|
||||
status = Py_InitializeFromConfig(&pyconfig);
|
||||
PyConfig_Clear(&pyconfig);
|
||||
|
@ -241,9 +260,11 @@ void McRFPy_API::setSpriteTexture(int ti)
|
|||
void McRFPy_API::api_init() {
|
||||
|
||||
// build API exposure before python initialization
|
||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||
// use full path version of argv[0] from OS to init python
|
||||
init_python(narrow_string(executable_filename()).c_str());
|
||||
if (!Py_IsInitialized()) {
|
||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||
// use full path version of argv[0] from OS to init python
|
||||
init_python(narrow_string(executable_filename()).c_str());
|
||||
}
|
||||
|
||||
//texture.loadFromFile("./assets/kenney_tinydungeon.png");
|
||||
//texture_size = 16, texture_width = 12, texture_height= 11;
|
||||
|
|
|
@ -0,0 +1,817 @@
|
|||
#include "McRFPy_Automation.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "GameEngine.h"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
|
||||
// Helper function to get game engine
|
||||
GameEngine* McRFPy_Automation::getGameEngine() {
|
||||
return McRFPy_API::game;
|
||||
}
|
||||
|
||||
// Sleep helper
|
||||
void McRFPy_Automation::sleep_ms(int milliseconds) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
|
||||
}
|
||||
|
||||
// Convert string to SFML key code
|
||||
sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) {
|
||||
static const std::unordered_map<std::string, sf::Keyboard::Key> keyMap = {
|
||||
// Letters
|
||||
{"a", sf::Keyboard::A}, {"b", sf::Keyboard::B}, {"c", sf::Keyboard::C},
|
||||
{"d", sf::Keyboard::D}, {"e", sf::Keyboard::E}, {"f", sf::Keyboard::F},
|
||||
{"g", sf::Keyboard::G}, {"h", sf::Keyboard::H}, {"i", sf::Keyboard::I},
|
||||
{"j", sf::Keyboard::J}, {"k", sf::Keyboard::K}, {"l", sf::Keyboard::L},
|
||||
{"m", sf::Keyboard::M}, {"n", sf::Keyboard::N}, {"o", sf::Keyboard::O},
|
||||
{"p", sf::Keyboard::P}, {"q", sf::Keyboard::Q}, {"r", sf::Keyboard::R},
|
||||
{"s", sf::Keyboard::S}, {"t", sf::Keyboard::T}, {"u", sf::Keyboard::U},
|
||||
{"v", sf::Keyboard::V}, {"w", sf::Keyboard::W}, {"x", sf::Keyboard::X},
|
||||
{"y", sf::Keyboard::Y}, {"z", sf::Keyboard::Z},
|
||||
|
||||
// Numbers
|
||||
{"0", sf::Keyboard::Num0}, {"1", sf::Keyboard::Num1}, {"2", sf::Keyboard::Num2},
|
||||
{"3", sf::Keyboard::Num3}, {"4", sf::Keyboard::Num4}, {"5", sf::Keyboard::Num5},
|
||||
{"6", sf::Keyboard::Num6}, {"7", sf::Keyboard::Num7}, {"8", sf::Keyboard::Num8},
|
||||
{"9", sf::Keyboard::Num9},
|
||||
|
||||
// Function keys
|
||||
{"f1", sf::Keyboard::F1}, {"f2", sf::Keyboard::F2}, {"f3", sf::Keyboard::F3},
|
||||
{"f4", sf::Keyboard::F4}, {"f5", sf::Keyboard::F5}, {"f6", sf::Keyboard::F6},
|
||||
{"f7", sf::Keyboard::F7}, {"f8", sf::Keyboard::F8}, {"f9", sf::Keyboard::F9},
|
||||
{"f10", sf::Keyboard::F10}, {"f11", sf::Keyboard::F11}, {"f12", sf::Keyboard::F12},
|
||||
{"f13", sf::Keyboard::F13}, {"f14", sf::Keyboard::F14}, {"f15", sf::Keyboard::F15},
|
||||
|
||||
// Special keys
|
||||
{"escape", sf::Keyboard::Escape}, {"esc", sf::Keyboard::Escape},
|
||||
{"enter", sf::Keyboard::Enter}, {"return", sf::Keyboard::Enter},
|
||||
{"space", sf::Keyboard::Space}, {" ", sf::Keyboard::Space},
|
||||
{"tab", sf::Keyboard::Tab}, {"\t", sf::Keyboard::Tab},
|
||||
{"backspace", sf::Keyboard::BackSpace},
|
||||
{"delete", sf::Keyboard::Delete}, {"del", sf::Keyboard::Delete},
|
||||
{"insert", sf::Keyboard::Insert},
|
||||
{"home", sf::Keyboard::Home},
|
||||
{"end", sf::Keyboard::End},
|
||||
{"pageup", sf::Keyboard::PageUp}, {"pgup", sf::Keyboard::PageUp},
|
||||
{"pagedown", sf::Keyboard::PageDown}, {"pgdn", sf::Keyboard::PageDown},
|
||||
|
||||
// Arrow keys
|
||||
{"left", sf::Keyboard::Left},
|
||||
{"right", sf::Keyboard::Right},
|
||||
{"up", sf::Keyboard::Up},
|
||||
{"down", sf::Keyboard::Down},
|
||||
|
||||
// Modifiers
|
||||
{"ctrl", sf::Keyboard::LControl}, {"ctrlleft", sf::Keyboard::LControl},
|
||||
{"ctrlright", sf::Keyboard::RControl},
|
||||
{"alt", sf::Keyboard::LAlt}, {"altleft", sf::Keyboard::LAlt},
|
||||
{"altright", sf::Keyboard::RAlt},
|
||||
{"shift", sf::Keyboard::LShift}, {"shiftleft", sf::Keyboard::LShift},
|
||||
{"shiftright", sf::Keyboard::RShift},
|
||||
{"win", sf::Keyboard::LSystem}, {"winleft", sf::Keyboard::LSystem},
|
||||
{"winright", sf::Keyboard::RSystem}, {"command", sf::Keyboard::LSystem},
|
||||
|
||||
// Punctuation
|
||||
{",", sf::Keyboard::Comma}, {".", sf::Keyboard::Period},
|
||||
{"/", sf::Keyboard::Slash}, {"\\", sf::Keyboard::BackSlash},
|
||||
{";", sf::Keyboard::SemiColon}, {"'", sf::Keyboard::Quote},
|
||||
{"[", sf::Keyboard::LBracket}, {"]", sf::Keyboard::RBracket},
|
||||
{"-", sf::Keyboard::Dash}, {"=", sf::Keyboard::Equal},
|
||||
|
||||
// Numpad
|
||||
{"num0", sf::Keyboard::Numpad0}, {"num1", sf::Keyboard::Numpad1},
|
||||
{"num2", sf::Keyboard::Numpad2}, {"num3", sf::Keyboard::Numpad3},
|
||||
{"num4", sf::Keyboard::Numpad4}, {"num5", sf::Keyboard::Numpad5},
|
||||
{"num6", sf::Keyboard::Numpad6}, {"num7", sf::Keyboard::Numpad7},
|
||||
{"num8", sf::Keyboard::Numpad8}, {"num9", sf::Keyboard::Numpad9},
|
||||
{"add", sf::Keyboard::Add}, {"subtract", sf::Keyboard::Subtract},
|
||||
{"multiply", sf::Keyboard::Multiply}, {"divide", sf::Keyboard::Divide},
|
||||
|
||||
// Other
|
||||
{"pause", sf::Keyboard::Pause},
|
||||
{"capslock", sf::Keyboard::LControl}, // Note: SFML doesn't have CapsLock
|
||||
{"numlock", sf::Keyboard::LControl}, // Note: SFML doesn't have NumLock
|
||||
{"scrolllock", sf::Keyboard::LControl}, // Note: SFML doesn't have ScrollLock
|
||||
};
|
||||
|
||||
auto it = keyMap.find(keyName);
|
||||
if (it != keyMap.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return sf::Keyboard::Unknown;
|
||||
}
|
||||
|
||||
// Inject mouse event into the game engine
|
||||
void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine) return;
|
||||
|
||||
sf::Event event;
|
||||
event.type = type;
|
||||
|
||||
switch (type) {
|
||||
case sf::Event::MouseMoved:
|
||||
event.mouseMove.x = x;
|
||||
event.mouseMove.y = y;
|
||||
break;
|
||||
case sf::Event::MouseButtonPressed:
|
||||
case sf::Event::MouseButtonReleased:
|
||||
event.mouseButton.button = button;
|
||||
event.mouseButton.x = x;
|
||||
event.mouseButton.y = y;
|
||||
break;
|
||||
case sf::Event::MouseWheelScrolled:
|
||||
event.mouseWheelScroll.wheel = sf::Mouse::VerticalWheel;
|
||||
event.mouseWheelScroll.delta = static_cast<float>(x); // x is used for scroll amount
|
||||
event.mouseWheelScroll.x = x;
|
||||
event.mouseWheelScroll.y = y;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
engine->processEvent(event);
|
||||
}
|
||||
|
||||
// Inject keyboard event into the game engine
|
||||
void McRFPy_Automation::injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine) return;
|
||||
|
||||
sf::Event event;
|
||||
event.type = type;
|
||||
|
||||
if (type == sf::Event::KeyPressed || type == sf::Event::KeyReleased) {
|
||||
event.key.code = key;
|
||||
event.key.alt = sf::Keyboard::isKeyPressed(sf::Keyboard::LAlt) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RAlt);
|
||||
event.key.control = sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RControl);
|
||||
event.key.shift = sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RShift);
|
||||
event.key.system = sf::Keyboard::isKeyPressed(sf::Keyboard::LSystem) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RSystem);
|
||||
}
|
||||
|
||||
engine->processEvent(event);
|
||||
}
|
||||
|
||||
// Inject text event for typing
|
||||
void McRFPy_Automation::injectTextEvent(sf::Uint32 unicode) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine) return;
|
||||
|
||||
sf::Event event;
|
||||
event.type = sf::Event::TextEntered;
|
||||
event.text.unicode = unicode;
|
||||
|
||||
engine->processEvent(event);
|
||||
}
|
||||
|
||||
// Screenshot implementation
|
||||
PyObject* McRFPy_Automation::_screenshot(PyObject* self, PyObject* args) {
|
||||
const char* filename;
|
||||
if (!PyArg_ParseTuple(args, "s", &filename)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto engine = getGameEngine();
|
||||
if (!engine) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Game engine not initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the render target
|
||||
sf::RenderTarget* target = engine->getRenderTargetPtr();
|
||||
if (!target) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No render target available");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// For RenderWindow, we can get a screenshot directly
|
||||
if (auto* window = dynamic_cast<sf::RenderWindow*>(target)) {
|
||||
sf::Vector2u windowSize = window->getSize();
|
||||
sf::Texture texture;
|
||||
texture.create(windowSize.x, windowSize.y);
|
||||
texture.update(*window);
|
||||
|
||||
if (texture.copyToImage().saveToFile(filename)) {
|
||||
Py_RETURN_TRUE;
|
||||
} else {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
}
|
||||
// For RenderTexture (headless mode)
|
||||
else if (auto* renderTexture = dynamic_cast<sf::RenderTexture*>(target)) {
|
||||
if (renderTexture->getTexture().copyToImage().saveToFile(filename)) {
|
||||
Py_RETURN_TRUE;
|
||||
} else {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_RuntimeError, "Unknown render target type");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get current mouse position
|
||||
PyObject* McRFPy_Automation::_position(PyObject* self, PyObject* args) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine || !engine->getRenderTargetPtr()) {
|
||||
return Py_BuildValue("(ii)", 0, 0);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// In headless mode, return simulated position (TODO: track this)
|
||||
return Py_BuildValue("(ii)", 0, 0);
|
||||
}
|
||||
|
||||
// Get screen size
|
||||
PyObject* McRFPy_Automation::_size(PyObject* self, PyObject* args) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine || !engine->getRenderTargetPtr()) {
|
||||
return Py_BuildValue("(ii)", 1024, 768); // Default size
|
||||
}
|
||||
|
||||
sf::Vector2u size = engine->getRenderTarget().getSize();
|
||||
return Py_BuildValue("(ii)", size.x, size.y);
|
||||
}
|
||||
|
||||
// Check if coordinates are on screen
|
||||
PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args) {
|
||||
int x, y;
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto engine = getGameEngine();
|
||||
if (!engine || !engine->getRenderTargetPtr()) {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
sf::Vector2u size = engine->getRenderTarget().getSize();
|
||||
if (x >= 0 && x < (int)size.x && y >= 0 && y < (int)size.y) {
|
||||
Py_RETURN_TRUE;
|
||||
} else {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
// Move mouse to position
|
||||
PyObject* McRFPy_Automation::_moveTo(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "duration", NULL};
|
||||
int x, y;
|
||||
float duration = 0.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
|
||||
&x, &y, &duration)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// TODO: Implement smooth movement with duration
|
||||
injectMouseEvent(sf::Event::MouseMoved, x, y);
|
||||
|
||||
if (duration > 0) {
|
||||
sleep_ms(static_cast<int>(duration * 1000));
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Move mouse relative
|
||||
PyObject* McRFPy_Automation::_moveRel(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"xOffset", "yOffset", "duration", NULL};
|
||||
int xOffset, yOffset;
|
||||
float duration = 0.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
|
||||
&xOffset, &yOffset, &duration)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get current position
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
int currentX, currentY;
|
||||
if (!PyArg_ParseTuple(pos, "ii", ¤tX, ¤tY)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
|
||||
// Move to new position
|
||||
injectMouseEvent(sf::Event::MouseMoved, currentX + xOffset, currentY + yOffset);
|
||||
|
||||
if (duration > 0) {
|
||||
sleep_ms(static_cast<int>(duration * 1000));
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Click implementation
|
||||
PyObject* McRFPy_Automation::_click(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "clicks", "interval", "button", NULL};
|
||||
int x = -1, y = -1;
|
||||
int clicks = 1;
|
||||
float interval = 0.0f;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iiifs", const_cast<char**>(kwlist),
|
||||
&x, &y, &clicks, &interval, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
}
|
||||
|
||||
// Determine button
|
||||
sf::Mouse::Button sfButton = sf::Mouse::Left;
|
||||
if (strcmp(button, "right") == 0) {
|
||||
sfButton = sf::Mouse::Right;
|
||||
} else if (strcmp(button, "middle") == 0) {
|
||||
sfButton = sf::Mouse::Middle;
|
||||
}
|
||||
|
||||
// Move to position first
|
||||
injectMouseEvent(sf::Event::MouseMoved, x, y);
|
||||
|
||||
// Perform clicks
|
||||
for (int i = 0; i < clicks; i++) {
|
||||
if (i > 0 && interval > 0) {
|
||||
sleep_ms(static_cast<int>(interval * 1000));
|
||||
}
|
||||
|
||||
injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton);
|
||||
sleep_ms(10); // Small delay between press and release
|
||||
injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton);
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Right click
|
||||
PyObject* McRFPy_Automation::_rightClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Build new args with button="right"
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("right"));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Double click
|
||||
PyObject* McRFPy_Automation::_doubleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(2));
|
||||
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Type text
|
||||
PyObject* McRFPy_Automation::_typewrite(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"message", "interval", NULL};
|
||||
const char* message;
|
||||
float interval = 0.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|f", const_cast<char**>(kwlist),
|
||||
&message, &interval)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Type each character
|
||||
for (size_t i = 0; message[i] != '\0'; i++) {
|
||||
if (i > 0 && interval > 0) {
|
||||
sleep_ms(static_cast<int>(interval * 1000));
|
||||
}
|
||||
|
||||
char c = message[i];
|
||||
|
||||
// Handle special characters
|
||||
if (c == '\n') {
|
||||
injectKeyEvent(sf::Event::KeyPressed, sf::Keyboard::Enter);
|
||||
injectKeyEvent(sf::Event::KeyReleased, sf::Keyboard::Enter);
|
||||
} else if (c == '\t') {
|
||||
injectKeyEvent(sf::Event::KeyPressed, sf::Keyboard::Tab);
|
||||
injectKeyEvent(sf::Event::KeyReleased, sf::Keyboard::Tab);
|
||||
} else {
|
||||
// For regular characters, send text event
|
||||
injectTextEvent(static_cast<sf::Uint32>(c));
|
||||
}
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Press and hold key
|
||||
PyObject* McRFPy_Automation::_keyDown(PyObject* self, PyObject* args) {
|
||||
const char* keyName;
|
||||
if (!PyArg_ParseTuple(args, "s", &keyName)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sf::Keyboard::Key key = stringToKey(keyName);
|
||||
if (key == sf::Keyboard::Unknown) {
|
||||
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
injectKeyEvent(sf::Event::KeyPressed, key);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Release key
|
||||
PyObject* McRFPy_Automation::_keyUp(PyObject* self, PyObject* args) {
|
||||
const char* keyName;
|
||||
if (!PyArg_ParseTuple(args, "s", &keyName)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sf::Keyboard::Key key = stringToKey(keyName);
|
||||
if (key == sf::Keyboard::Unknown) {
|
||||
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
injectKeyEvent(sf::Event::KeyReleased, key);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Hotkey combination
|
||||
PyObject* McRFPy_Automation::_hotkey(PyObject* self, PyObject* args) {
|
||||
// Get all keys as separate arguments
|
||||
Py_ssize_t numKeys = PyTuple_Size(args);
|
||||
if (numKeys == 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "hotkey() requires at least one key");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Press all keys
|
||||
for (Py_ssize_t i = 0; i < numKeys; i++) {
|
||||
PyObject* keyObj = PyTuple_GetItem(args, i);
|
||||
const char* keyName = PyUnicode_AsUTF8(keyObj);
|
||||
if (!keyName) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sf::Keyboard::Key key = stringToKey(keyName);
|
||||
if (key == sf::Keyboard::Unknown) {
|
||||
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
injectKeyEvent(sf::Event::KeyPressed, key);
|
||||
sleep_ms(10); // Small delay between key presses
|
||||
}
|
||||
|
||||
// Release all keys in reverse order
|
||||
for (Py_ssize_t i = numKeys - 1; i >= 0; i--) {
|
||||
PyObject* keyObj = PyTuple_GetItem(args, i);
|
||||
const char* keyName = PyUnicode_AsUTF8(keyObj);
|
||||
|
||||
sf::Keyboard::Key key = stringToKey(keyName);
|
||||
injectKeyEvent(sf::Event::KeyReleased, key);
|
||||
sleep_ms(10);
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Scroll wheel
|
||||
PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"clicks", "x", "y", NULL};
|
||||
int clicks;
|
||||
int x = -1, y = -1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|ii", const_cast<char**>(kwlist),
|
||||
&clicks, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
}
|
||||
|
||||
// Inject scroll event
|
||||
injectMouseEvent(sf::Event::MouseWheelScrolled, clicks, y);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Other click types using the main click function
|
||||
PyObject* McRFPy_Automation::_middleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("middle"));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
return result;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_Automation::_tripleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(3));
|
||||
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Mouse button press/release
|
||||
PyObject* McRFPy_Automation::_mouseDown(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "button", NULL};
|
||||
int x = -1, y = -1;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
|
||||
&x, &y, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
}
|
||||
|
||||
sf::Mouse::Button sfButton = sf::Mouse::Left;
|
||||
if (strcmp(button, "right") == 0) {
|
||||
sfButton = sf::Mouse::Right;
|
||||
} else if (strcmp(button, "middle") == 0) {
|
||||
sfButton = sf::Mouse::Middle;
|
||||
}
|
||||
|
||||
injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_Automation::_mouseUp(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "button", NULL};
|
||||
int x = -1, y = -1;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
|
||||
&x, &y, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
}
|
||||
|
||||
sf::Mouse::Button sfButton = sf::Mouse::Left;
|
||||
if (strcmp(button, "right") == 0) {
|
||||
sfButton = sf::Mouse::Right;
|
||||
} else if (strcmp(button, "middle") == 0) {
|
||||
sfButton = sf::Mouse::Middle;
|
||||
}
|
||||
|
||||
injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Drag operations
|
||||
PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "duration", "button", NULL};
|
||||
int x, y;
|
||||
float duration = 0.0f;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
|
||||
&x, &y, &duration, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get current position
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
int startX, startY;
|
||||
if (!PyArg_ParseTuple(pos, "ii", &startX, &startY)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
|
||||
// Mouse down at current position
|
||||
PyObject* downArgs = Py_BuildValue("(ii)", startX, startY);
|
||||
PyObject* downKwargs = PyDict_New();
|
||||
PyDict_SetItemString(downKwargs, "button", PyUnicode_FromString(button));
|
||||
|
||||
PyObject* downResult = _mouseDown(self, downArgs, downKwargs);
|
||||
Py_DECREF(downArgs);
|
||||
Py_DECREF(downKwargs);
|
||||
if (!downResult) return NULL;
|
||||
Py_DECREF(downResult);
|
||||
|
||||
// Move to target position
|
||||
if (duration > 0) {
|
||||
// Smooth movement
|
||||
int steps = static_cast<int>(duration * 60); // 60 FPS
|
||||
for (int i = 1; i <= steps; i++) {
|
||||
int currentX = startX + (x - startX) * i / steps;
|
||||
int currentY = startY + (y - startY) * i / steps;
|
||||
injectMouseEvent(sf::Event::MouseMoved, currentX, currentY);
|
||||
sleep_ms(1000 / 60); // 60 FPS
|
||||
}
|
||||
} else {
|
||||
injectMouseEvent(sf::Event::MouseMoved, x, y);
|
||||
}
|
||||
|
||||
// Mouse up at target position
|
||||
PyObject* upArgs = Py_BuildValue("(ii)", x, y);
|
||||
PyObject* upKwargs = PyDict_New();
|
||||
PyDict_SetItemString(upKwargs, "button", PyUnicode_FromString(button));
|
||||
|
||||
PyObject* upResult = _mouseUp(self, upArgs, upKwargs);
|
||||
Py_DECREF(upArgs);
|
||||
Py_DECREF(upKwargs);
|
||||
if (!upResult) return NULL;
|
||||
Py_DECREF(upResult);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_Automation::_dragRel(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"xOffset", "yOffset", "duration", "button", NULL};
|
||||
int xOffset, yOffset;
|
||||
float duration = 0.0f;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
|
||||
&xOffset, &yOffset, &duration, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get current position
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
int currentX, currentY;
|
||||
if (!PyArg_ParseTuple(pos, "ii", ¤tX, ¤tY)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
|
||||
// Call dragTo with absolute position
|
||||
PyObject* dragArgs = Py_BuildValue("(ii)", currentX + xOffset, currentY + yOffset);
|
||||
PyObject* dragKwargs = PyDict_New();
|
||||
PyDict_SetItemString(dragKwargs, "duration", PyFloat_FromDouble(duration));
|
||||
PyDict_SetItemString(dragKwargs, "button", PyUnicode_FromString(button));
|
||||
|
||||
PyObject* result = _dragTo(self, dragArgs, dragKwargs);
|
||||
Py_DECREF(dragArgs);
|
||||
Py_DECREF(dragKwargs);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Method definitions for the automation module
|
||||
static PyMethodDef automationMethods[] = {
|
||||
{"screenshot", McRFPy_Automation::_screenshot, METH_VARARGS,
|
||||
"screenshot(filename) - Save a screenshot to the specified file"},
|
||||
|
||||
{"position", McRFPy_Automation::_position, METH_NOARGS,
|
||||
"position() - Get current mouse position as (x, y) tuple"},
|
||||
{"size", McRFPy_Automation::_size, METH_NOARGS,
|
||||
"size() - Get screen size as (width, height) tuple"},
|
||||
{"onScreen", McRFPy_Automation::_onScreen, METH_VARARGS,
|
||||
"onScreen(x, y) - Check if coordinates are within screen bounds"},
|
||||
|
||||
{"moveTo", (PyCFunction)McRFPy_Automation::_moveTo, METH_VARARGS | METH_KEYWORDS,
|
||||
"moveTo(x, y, duration=0.0) - Move mouse to absolute position"},
|
||||
{"moveRel", (PyCFunction)McRFPy_Automation::_moveRel, METH_VARARGS | METH_KEYWORDS,
|
||||
"moveRel(xOffset, yOffset, duration=0.0) - Move mouse relative to current position"},
|
||||
{"dragTo", (PyCFunction)McRFPy_Automation::_dragTo, METH_VARARGS | METH_KEYWORDS,
|
||||
"dragTo(x, y, duration=0.0, button='left') - Drag mouse to position"},
|
||||
{"dragRel", (PyCFunction)McRFPy_Automation::_dragRel, METH_VARARGS | METH_KEYWORDS,
|
||||
"dragRel(xOffset, yOffset, duration=0.0, button='left') - Drag mouse relative to current position"},
|
||||
|
||||
{"click", (PyCFunction)McRFPy_Automation::_click, METH_VARARGS | METH_KEYWORDS,
|
||||
"click(x=None, y=None, clicks=1, interval=0.0, button='left') - Click at position"},
|
||||
{"rightClick", (PyCFunction)McRFPy_Automation::_rightClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"rightClick(x=None, y=None) - Right click at position"},
|
||||
{"middleClick", (PyCFunction)McRFPy_Automation::_middleClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"middleClick(x=None, y=None) - Middle click at position"},
|
||||
{"doubleClick", (PyCFunction)McRFPy_Automation::_doubleClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"doubleClick(x=None, y=None) - Double click at position"},
|
||||
{"tripleClick", (PyCFunction)McRFPy_Automation::_tripleClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"tripleClick(x=None, y=None) - Triple click at position"},
|
||||
{"scroll", (PyCFunction)McRFPy_Automation::_scroll, METH_VARARGS | METH_KEYWORDS,
|
||||
"scroll(clicks, x=None, y=None) - Scroll wheel at position"},
|
||||
{"mouseDown", (PyCFunction)McRFPy_Automation::_mouseDown, METH_VARARGS | METH_KEYWORDS,
|
||||
"mouseDown(x=None, y=None, button='left') - Press mouse button"},
|
||||
{"mouseUp", (PyCFunction)McRFPy_Automation::_mouseUp, METH_VARARGS | METH_KEYWORDS,
|
||||
"mouseUp(x=None, y=None, button='left') - Release mouse button"},
|
||||
|
||||
{"typewrite", (PyCFunction)McRFPy_Automation::_typewrite, METH_VARARGS | METH_KEYWORDS,
|
||||
"typewrite(message, interval=0.0) - Type text with optional interval between keystrokes"},
|
||||
{"hotkey", McRFPy_Automation::_hotkey, METH_VARARGS,
|
||||
"hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c'))"},
|
||||
{"keyDown", McRFPy_Automation::_keyDown, METH_VARARGS,
|
||||
"keyDown(key) - Press and hold a key"},
|
||||
{"keyUp", McRFPy_Automation::_keyUp, METH_VARARGS,
|
||||
"keyUp(key) - Release a key"},
|
||||
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
// Module definition for mcrfpy.automation
|
||||
static PyModuleDef automationModule = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
"mcrfpy.automation",
|
||||
"Automation API for McRogueFace - PyAutoGUI-compatible interface",
|
||||
-1,
|
||||
automationMethods
|
||||
};
|
||||
|
||||
// Initialize automation submodule
|
||||
PyObject* McRFPy_Automation::init_automation_module() {
|
||||
PyObject* module = PyModule_Create(&automationModule);
|
||||
if (module == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Window.hpp>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
class GameEngine;
|
||||
|
||||
class McRFPy_Automation {
|
||||
public:
|
||||
// Initialize the automation submodule
|
||||
static PyObject* init_automation_module();
|
||||
|
||||
// Screenshot functionality
|
||||
static PyObject* _screenshot(PyObject* self, PyObject* args);
|
||||
|
||||
// Mouse position and screen info
|
||||
static PyObject* _position(PyObject* self, PyObject* args);
|
||||
static PyObject* _size(PyObject* self, PyObject* args);
|
||||
static PyObject* _onScreen(PyObject* self, PyObject* args);
|
||||
|
||||
// Mouse movement
|
||||
static PyObject* _moveTo(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _moveRel(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _dragTo(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _dragRel(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
|
||||
// Mouse clicks
|
||||
static PyObject* _click(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _rightClick(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _middleClick(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _doubleClick(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _tripleClick(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _scroll(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _mouseDown(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _mouseUp(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
|
||||
// Keyboard
|
||||
static PyObject* _typewrite(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _hotkey(PyObject* self, PyObject* args);
|
||||
static PyObject* _keyDown(PyObject* self, PyObject* args);
|
||||
static PyObject* _keyUp(PyObject* self, PyObject* args);
|
||||
|
||||
// Helper functions
|
||||
static void injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button = sf::Mouse::Left);
|
||||
static void injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key);
|
||||
static void injectTextEvent(sf::Uint32 unicode);
|
||||
static sf::Keyboard::Key stringToKey(const std::string& keyName);
|
||||
static void sleep_ms(int milliseconds);
|
||||
|
||||
private:
|
||||
static GameEngine* getGameEngine();
|
||||
};
|
|
@ -22,6 +22,9 @@ struct McRogueFaceConfig {
|
|||
std::filesystem::path script_path;
|
||||
std::vector<std::string> script_args;
|
||||
|
||||
// Scripts to execute before main script (--exec flag)
|
||||
std::vector<std::filesystem::path> exec_scripts;
|
||||
|
||||
// Screenshot functionality for headless mode
|
||||
std::string screenshot_path;
|
||||
bool take_screenshot = false;
|
||||
|
|
70
src/main.cpp
70
src/main.cpp
|
@ -44,15 +44,53 @@ int run_game_engine(const McRogueFaceConfig& config)
|
|||
|
||||
int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[])
|
||||
{
|
||||
// Create a headless game engine for automation API support
|
||||
McRogueFaceConfig engine_config = config;
|
||||
engine_config.headless = true; // Force headless mode for Python interpreter
|
||||
GameEngine* engine = new GameEngine(engine_config);
|
||||
|
||||
// Initialize Python with configuration
|
||||
McRFPy_API::init_python_with_config(config, argc, argv);
|
||||
|
||||
// Handle different Python modes
|
||||
if (!config.python_command.empty()) {
|
||||
// Execute command from -c
|
||||
int result = PyRun_SimpleString(config.python_command.c_str());
|
||||
Py_Finalize();
|
||||
return result;
|
||||
if (config.interactive_mode) {
|
||||
// Use PyRun_String to catch SystemExit
|
||||
PyObject* main_module = PyImport_AddModule("__main__");
|
||||
PyObject* main_dict = PyModule_GetDict(main_module);
|
||||
PyObject* result_obj = PyRun_String(config.python_command.c_str(),
|
||||
Py_file_input, main_dict, main_dict);
|
||||
|
||||
if (result_obj == NULL) {
|
||||
// Check if it's SystemExit
|
||||
if (PyErr_Occurred()) {
|
||||
PyObject *type, *value, *traceback;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
|
||||
// If it's SystemExit and we're in interactive mode, clear it
|
||||
if (PyErr_GivenExceptionMatches(type, PyExc_SystemExit)) {
|
||||
PyErr_Clear();
|
||||
} else {
|
||||
// Re-raise other exceptions
|
||||
PyErr_Restore(type, value, traceback);
|
||||
PyErr_Print();
|
||||
}
|
||||
|
||||
Py_XDECREF(type);
|
||||
Py_XDECREF(value);
|
||||
Py_XDECREF(traceback);
|
||||
}
|
||||
} else {
|
||||
Py_DECREF(result_obj);
|
||||
}
|
||||
// Continue to interactive mode below
|
||||
} else {
|
||||
int result = PyRun_SimpleString(config.python_command.c_str());
|
||||
Py_Finalize();
|
||||
delete engine;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else if (!config.python_module.empty()) {
|
||||
// Execute module using runpy
|
||||
|
@ -69,6 +107,7 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
|||
|
||||
int result = PyRun_SimpleString(run_module_code.c_str());
|
||||
Py_Finalize();
|
||||
delete engine;
|
||||
return result;
|
||||
}
|
||||
else if (!config.script_path.empty()) {
|
||||
|
@ -97,12 +136,33 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
|||
}
|
||||
delete[] python_argv;
|
||||
|
||||
if (config.interactive_mode && result == 0) {
|
||||
if (config.interactive_mode) {
|
||||
// Even if script had SystemExit, continue to interactive mode
|
||||
if (result != 0) {
|
||||
// Check if it was SystemExit
|
||||
if (PyErr_Occurred()) {
|
||||
PyObject *type, *value, *traceback;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
|
||||
if (PyErr_GivenExceptionMatches(type, PyExc_SystemExit)) {
|
||||
PyErr_Clear();
|
||||
result = 0; // Don't exit with error
|
||||
} else {
|
||||
PyErr_Restore(type, value, traceback);
|
||||
PyErr_Print();
|
||||
}
|
||||
|
||||
Py_XDECREF(type);
|
||||
Py_XDECREF(value);
|
||||
Py_XDECREF(traceback);
|
||||
}
|
||||
}
|
||||
// Run interactive mode after script
|
||||
PyRun_InteractiveLoop(stdin, "<stdin>");
|
||||
}
|
||||
|
||||
Py_Finalize();
|
||||
delete engine;
|
||||
return result;
|
||||
}
|
||||
else if (config.interactive_mode || config.python_mode) {
|
||||
|
@ -110,8 +170,10 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
|||
Py_InspectFlag = 1;
|
||||
PyRun_InteractiveLoop(stdin, "<stdin>");
|
||||
Py_Finalize();
|
||||
delete engine;
|
||||
return 0;
|
||||
}
|
||||
|
||||
delete engine;
|
||||
return 0;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue