diff --git a/automation_example.py b/automation_example.py new file mode 100644 index 0000000..5d94dc4 --- /dev/null +++ b/automation_example.py @@ -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!") \ No newline at end of file diff --git a/automation_exec_examples.py b/automation_exec_examples.py new file mode 100644 index 0000000..1145d2b --- /dev/null +++ b/automation_exec_examples.py @@ -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") \ No newline at end of file diff --git a/example_automation.py b/example_automation.py new file mode 100644 index 0000000..a31375a --- /dev/null +++ b/example_automation.py @@ -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") \ No newline at end of file diff --git a/example_config.py b/example_config.py new file mode 100644 index 0000000..0f0ef7e --- /dev/null +++ b/example_config.py @@ -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") \ No newline at end of file diff --git a/example_monitoring.py b/example_monitoring.py new file mode 100644 index 0000000..13e98cb --- /dev/null +++ b/example_monitoring.py @@ -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") \ No newline at end of file diff --git a/exec_flag_implementation.cpp b/exec_flag_implementation.cpp new file mode 100644 index 0000000..3173585 --- /dev/null +++ b/exec_flag_implementation.cpp @@ -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 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 \ No newline at end of file diff --git a/src/ActionCode.h b/src/ActionCode.h index 36aca07..1adaf99 100644 --- a/src/ActionCode.h +++ b/src/ActionCode.h @@ -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) { diff --git a/src/CommandLineParser.cpp b/src/CommandLineParser.cpp index cabf47d..3e69b1b 100644 --- a/src/CommandLineParser.cpp +++ b/src/CommandLineParser.cpp @@ -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" diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 8495810..09d8329 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -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 <<")"< " << (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); } } diff --git a/src/GameEngine.h b/src/GameEngine.h index 326823d..02e02ae 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -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 textures; diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 37eb8c4..2f38916 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -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; diff --git a/src/McRFPy_Automation.cpp b/src/McRFPy_Automation.cpp new file mode 100644 index 0000000..f755921 --- /dev/null +++ b/src/McRFPy_Automation.cpp @@ -0,0 +1,817 @@ +#include "McRFPy_Automation.h" +#include "McRFPy_API.h" +#include "GameEngine.h" +#include +#include +#include +#include + +// 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 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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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; +} \ No newline at end of file diff --git a/src/McRFPy_Automation.h b/src/McRFPy_Automation.h new file mode 100644 index 0000000..fdf126e --- /dev/null +++ b/src/McRFPy_Automation.h @@ -0,0 +1,56 @@ +#pragma once +#include "Common.h" +#include "Python.h" +#include +#include +#include +#include +#include + +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(); +}; \ No newline at end of file diff --git a/src/McRogueFaceConfig.h b/src/McRogueFaceConfig.h index 659168d..34a589e 100644 --- a/src/McRogueFaceConfig.h +++ b/src/McRogueFaceConfig.h @@ -22,6 +22,9 @@ struct McRogueFaceConfig { std::filesystem::path script_path; std::vector script_args; + // Scripts to execute before main script (--exec flag) + std::vector exec_scripts; + // Screenshot functionality for headless mode std::string screenshot_path; bool take_screenshot = false; diff --git a/src/main.cpp b/src/main.cpp index 55a3b33..9745b60 100644 --- a/src/main.cpp +++ b/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, ""); } 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, ""); Py_Finalize(); + delete engine; return 0; } + delete engine; return 0; }