iflow-mcp_bethington-cheat-engine-server-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/METADATA +16 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/RECORD +40 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/WHEEL +5 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/top_level.txt +1 -0
- server/cheatengine/__init__.py +19 -0
- server/cheatengine/ce_bridge.py +1670 -0
- server/cheatengine/lua_interface.py +460 -0
- server/cheatengine/table_parser.py +1221 -0
- server/config/__init__.py +20 -0
- server/config/settings.py +347 -0
- server/config/whitelist.py +378 -0
- server/gui_automation/__init__.py +43 -0
- server/gui_automation/core/__init__.py +8 -0
- server/gui_automation/core/integration.py +951 -0
- server/gui_automation/demos/__init__.py +8 -0
- server/gui_automation/demos/basic_demo.py +754 -0
- server/gui_automation/demos/notepad_demo.py +460 -0
- server/gui_automation/demos/simple_demo.py +319 -0
- server/gui_automation/tools/__init__.py +8 -0
- server/gui_automation/tools/mcp_tools.py +974 -0
- server/main.py +519 -0
- server/memory/__init__.py +0 -0
- server/memory/analyzer.py +0 -0
- server/memory/reader.py +0 -0
- server/memory/scanner.py +0 -0
- server/memory/symbols.py +0 -0
- server/process/__init__.py +16 -0
- server/process/launcher.py +608 -0
- server/process/manager.py +185 -0
- server/process/monitors.py +202 -0
- server/process/permissions.py +131 -0
- server/process_whitelist.json +119 -0
- server/pyautogui/__init__.py +0 -0
- server/utils/__init__.py +37 -0
- server/utils/data_types.py +368 -0
- server/utils/formatters.py +430 -0
- server/utils/validators.py +340 -0
- server/window_automation/__init__.py +59 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP Cheat Engine Server - PyAutoGUI Integration Module
|
|
4
|
+
|
|
5
|
+
This module provides comprehensive access to all PyAutoGUI functionality
|
|
6
|
+
through the MCP Cheat Engine Server, including:
|
|
7
|
+
|
|
8
|
+
1. Screen automation (screenshots, pixel colors, image recognition)
|
|
9
|
+
2. Mouse control (movement, clicking, dragging, scrolling)
|
|
10
|
+
3. Keyboard automation (typing, key combinations, hotkeys)
|
|
11
|
+
4. Window management and screen utilities
|
|
12
|
+
5. Fail-safes and safety features
|
|
13
|
+
|
|
14
|
+
Design Principles:
|
|
15
|
+
- Complete PyAutoGUI API exposure through MCP tools
|
|
16
|
+
- Enhanced error handling and logging
|
|
17
|
+
- Security controls and validation
|
|
18
|
+
- Performance optimizations for image operations
|
|
19
|
+
- Integration with existing automation system
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
import logging
|
|
26
|
+
import threading
|
|
27
|
+
from typing import Dict, Any, List, Optional, Tuple, Union
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
import base64
|
|
31
|
+
import io
|
|
32
|
+
|
|
33
|
+
# PyAutoGUI and image processing imports
|
|
34
|
+
try:
|
|
35
|
+
import pyautogui
|
|
36
|
+
import PIL.Image
|
|
37
|
+
import cv2
|
|
38
|
+
import numpy as np
|
|
39
|
+
PYAUTOGUI_AVAILABLE = True
|
|
40
|
+
except ImportError as e:
|
|
41
|
+
PYAUTOGUI_AVAILABLE = False
|
|
42
|
+
print(f"PyAutoGUI dependencies not available: {e}")
|
|
43
|
+
|
|
44
|
+
# Configure logging
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ScreenInfo:
|
|
49
|
+
"""Screen information structure"""
|
|
50
|
+
width: int
|
|
51
|
+
height: int
|
|
52
|
+
primary_monitor: bool = True
|
|
53
|
+
monitor_count: int = 1
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class MouseInfo:
|
|
57
|
+
"""Mouse position and state information"""
|
|
58
|
+
x: int
|
|
59
|
+
y: int
|
|
60
|
+
timestamp: float = field(default_factory=time.time)
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ImageMatch:
|
|
64
|
+
"""Image recognition match result"""
|
|
65
|
+
found: bool
|
|
66
|
+
x: int = 0
|
|
67
|
+
y: int = 0
|
|
68
|
+
width: int = 0
|
|
69
|
+
height: int = 0
|
|
70
|
+
confidence: float = 0.0
|
|
71
|
+
center_x: int = 0
|
|
72
|
+
center_y: int = 0
|
|
73
|
+
|
|
74
|
+
def __post_init__(self):
|
|
75
|
+
if self.found:
|
|
76
|
+
self.center_x = self.x + self.width // 2
|
|
77
|
+
self.center_y = self.y + self.height // 2
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class AutomationResult:
|
|
81
|
+
"""Standardized result for PyAutoGUI operations"""
|
|
82
|
+
success: bool
|
|
83
|
+
operation: str
|
|
84
|
+
data: Dict = field(default_factory=dict)
|
|
85
|
+
error: str = ""
|
|
86
|
+
timestamp: float = field(default_factory=time.time)
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
89
|
+
"""Convert result to dictionary for JSON serialization"""
|
|
90
|
+
return {
|
|
91
|
+
"success": self.success,
|
|
92
|
+
"operation": self.operation,
|
|
93
|
+
"data": self.data,
|
|
94
|
+
"error": self.error,
|
|
95
|
+
"timestamp": self.timestamp
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class PyAutoGUIController:
|
|
99
|
+
"""Main controller for PyAutoGUI functionality"""
|
|
100
|
+
|
|
101
|
+
def __init__(self):
|
|
102
|
+
if not PYAUTOGUI_AVAILABLE:
|
|
103
|
+
raise ImportError("PyAutoGUI and required dependencies are not installed")
|
|
104
|
+
|
|
105
|
+
# Configure PyAutoGUI settings
|
|
106
|
+
self._configure_pyautogui()
|
|
107
|
+
|
|
108
|
+
# Initialize state tracking
|
|
109
|
+
self.screen_info = self._get_screen_info()
|
|
110
|
+
self.last_screenshot = None
|
|
111
|
+
self.screenshot_cache = {}
|
|
112
|
+
self.image_templates = {}
|
|
113
|
+
|
|
114
|
+
logger.info("PyAutoGUI Controller initialized successfully")
|
|
115
|
+
|
|
116
|
+
def _configure_pyautogui(self):
|
|
117
|
+
"""Configure PyAutoGUI with optimal settings"""
|
|
118
|
+
# Set fail-safe (move mouse to corner to abort)
|
|
119
|
+
pyautogui.FAILSAFE = True
|
|
120
|
+
|
|
121
|
+
# Set pause between actions (0.1 second default)
|
|
122
|
+
pyautogui.PAUSE = 0.1
|
|
123
|
+
|
|
124
|
+
# Set minimum duration for movements
|
|
125
|
+
pyautogui.MINIMUM_DURATION = 0.05
|
|
126
|
+
|
|
127
|
+
# Set minimum sleep time
|
|
128
|
+
pyautogui.MINIMUM_SLEEP = 0.05
|
|
129
|
+
|
|
130
|
+
logger.info("PyAutoGUI configured with safety settings")
|
|
131
|
+
|
|
132
|
+
def _get_screen_info(self) -> ScreenInfo:
|
|
133
|
+
"""Get current screen information"""
|
|
134
|
+
size = pyautogui.size()
|
|
135
|
+
return ScreenInfo(
|
|
136
|
+
width=size.width,
|
|
137
|
+
height=size.height,
|
|
138
|
+
primary_monitor=True,
|
|
139
|
+
monitor_count=1 # PyAutoGUI primarily works with primary monitor
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# ==========================================
|
|
143
|
+
# SCREEN CAPTURE AND ANALYSIS
|
|
144
|
+
# ==========================================
|
|
145
|
+
|
|
146
|
+
def take_screenshot(self, region: Optional[Tuple[int, int, int, int]] = None,
|
|
147
|
+
save_path: Optional[str] = None) -> AutomationResult:
|
|
148
|
+
"""Take a screenshot of the screen or a specific region"""
|
|
149
|
+
try:
|
|
150
|
+
if region:
|
|
151
|
+
# Take screenshot of specific region (left, top, width, height)
|
|
152
|
+
screenshot = pyautogui.screenshot(region=region)
|
|
153
|
+
else:
|
|
154
|
+
# Take full screen screenshot
|
|
155
|
+
screenshot = pyautogui.screenshot()
|
|
156
|
+
|
|
157
|
+
# Convert to base64 for transport
|
|
158
|
+
img_buffer = io.BytesIO()
|
|
159
|
+
screenshot.save(img_buffer, format='PNG')
|
|
160
|
+
img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
|
|
161
|
+
|
|
162
|
+
# Save to file if requested
|
|
163
|
+
if save_path:
|
|
164
|
+
screenshot.save(save_path)
|
|
165
|
+
|
|
166
|
+
# Cache the screenshot
|
|
167
|
+
self.last_screenshot = screenshot
|
|
168
|
+
cache_key = f"screenshot_{int(time.time())}"
|
|
169
|
+
self.screenshot_cache[cache_key] = screenshot
|
|
170
|
+
|
|
171
|
+
result_data = {
|
|
172
|
+
"image_base64": img_base64,
|
|
173
|
+
"width": screenshot.width,
|
|
174
|
+
"height": screenshot.height,
|
|
175
|
+
"region": region,
|
|
176
|
+
"save_path": save_path,
|
|
177
|
+
"cache_key": cache_key
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return AutomationResult(
|
|
181
|
+
success=True,
|
|
182
|
+
operation="take_screenshot",
|
|
183
|
+
data=result_data
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.error(f"Screenshot failed: {e}")
|
|
188
|
+
return AutomationResult(
|
|
189
|
+
success=False,
|
|
190
|
+
operation="take_screenshot",
|
|
191
|
+
error=str(e)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def get_pixel_color(self, x: int, y: int) -> AutomationResult:
|
|
195
|
+
"""Get the RGB color of a pixel at the specified coordinates"""
|
|
196
|
+
try:
|
|
197
|
+
# Validate coordinates
|
|
198
|
+
if not (0 <= x < self.screen_info.width and 0 <= y < self.screen_info.height):
|
|
199
|
+
raise ValueError(f"Coordinates ({x}, {y}) are outside screen bounds")
|
|
200
|
+
|
|
201
|
+
# Get pixel color
|
|
202
|
+
pixel_color = pyautogui.pixel(x, y)
|
|
203
|
+
|
|
204
|
+
# Convert to RGB values
|
|
205
|
+
r, g, b = pixel_color
|
|
206
|
+
hex_color = f"#{r:02x}{g:02x}{b:02x}"
|
|
207
|
+
|
|
208
|
+
return AutomationResult(
|
|
209
|
+
success=True,
|
|
210
|
+
operation="get_pixel_color",
|
|
211
|
+
data={
|
|
212
|
+
"x": x,
|
|
213
|
+
"y": y,
|
|
214
|
+
"rgb": [r, g, b],
|
|
215
|
+
"hex": hex_color,
|
|
216
|
+
"rgb_tuple": pixel_color
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.error(f"Get pixel color failed: {e}")
|
|
222
|
+
return AutomationResult(
|
|
223
|
+
success=False,
|
|
224
|
+
operation="get_pixel_color",
|
|
225
|
+
error=str(e)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def find_image_on_screen(self, image_path: str, confidence: float = 0.8,
|
|
229
|
+
region: Optional[Tuple[int, int, int, int]] = None) -> AutomationResult:
|
|
230
|
+
"""Find an image on the screen using template matching"""
|
|
231
|
+
try:
|
|
232
|
+
if not os.path.exists(image_path):
|
|
233
|
+
raise FileNotFoundError(f"Image file not found: {image_path}")
|
|
234
|
+
|
|
235
|
+
# Attempt to locate the image
|
|
236
|
+
try:
|
|
237
|
+
if region:
|
|
238
|
+
location = pyautogui.locateOnScreen(image_path, confidence=confidence, region=region)
|
|
239
|
+
else:
|
|
240
|
+
location = pyautogui.locateOnScreen(image_path, confidence=confidence)
|
|
241
|
+
|
|
242
|
+
if location:
|
|
243
|
+
# Image found
|
|
244
|
+
center = pyautogui.center(location)
|
|
245
|
+
|
|
246
|
+
match = ImageMatch(
|
|
247
|
+
found=True,
|
|
248
|
+
x=location.left,
|
|
249
|
+
y=location.top,
|
|
250
|
+
width=location.width,
|
|
251
|
+
height=location.height,
|
|
252
|
+
confidence=confidence,
|
|
253
|
+
center_x=center.x,
|
|
254
|
+
center_y=center.y
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return AutomationResult(
|
|
258
|
+
success=True,
|
|
259
|
+
operation="find_image_on_screen",
|
|
260
|
+
data={
|
|
261
|
+
"image_path": image_path,
|
|
262
|
+
"match": match.__dict__,
|
|
263
|
+
"region": region,
|
|
264
|
+
"search_confidence": confidence
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
# Image not found
|
|
269
|
+
match = ImageMatch(found=False)
|
|
270
|
+
|
|
271
|
+
return AutomationResult(
|
|
272
|
+
success=True,
|
|
273
|
+
operation="find_image_on_screen",
|
|
274
|
+
data={
|
|
275
|
+
"image_path": image_path,
|
|
276
|
+
"match": match.__dict__,
|
|
277
|
+
"region": region,
|
|
278
|
+
"search_confidence": confidence
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
except pyautogui.ImageNotFoundException:
|
|
283
|
+
# Image not found exception
|
|
284
|
+
match = ImageMatch(found=False)
|
|
285
|
+
|
|
286
|
+
return AutomationResult(
|
|
287
|
+
success=True,
|
|
288
|
+
operation="find_image_on_screen",
|
|
289
|
+
data={
|
|
290
|
+
"image_path": image_path,
|
|
291
|
+
"match": match.__dict__,
|
|
292
|
+
"region": region,
|
|
293
|
+
"search_confidence": confidence
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
except Exception as e:
|
|
298
|
+
logger.error(f"Image search failed: {e}")
|
|
299
|
+
return AutomationResult(
|
|
300
|
+
success=False,
|
|
301
|
+
operation="find_image_on_screen",
|
|
302
|
+
error=str(e)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def find_all_images_on_screen(self, image_path: str, confidence: float = 0.8,
|
|
306
|
+
region: Optional[Tuple[int, int, int, int]] = None) -> AutomationResult:
|
|
307
|
+
"""Find all instances of an image on the screen"""
|
|
308
|
+
try:
|
|
309
|
+
if not os.path.exists(image_path):
|
|
310
|
+
raise FileNotFoundError(f"Image file not found: {image_path}")
|
|
311
|
+
|
|
312
|
+
# Find all matches
|
|
313
|
+
try:
|
|
314
|
+
if region:
|
|
315
|
+
locations = list(pyautogui.locateAllOnScreen(image_path, confidence=confidence, region=region))
|
|
316
|
+
else:
|
|
317
|
+
locations = list(pyautogui.locateAllOnScreen(image_path, confidence=confidence))
|
|
318
|
+
|
|
319
|
+
matches = []
|
|
320
|
+
for location in locations:
|
|
321
|
+
center = pyautogui.center(location)
|
|
322
|
+
match = ImageMatch(
|
|
323
|
+
found=True,
|
|
324
|
+
x=location.left,
|
|
325
|
+
y=location.top,
|
|
326
|
+
width=location.width,
|
|
327
|
+
height=location.height,
|
|
328
|
+
confidence=confidence,
|
|
329
|
+
center_x=center.x,
|
|
330
|
+
center_y=center.y
|
|
331
|
+
)
|
|
332
|
+
matches.append(match.__dict__)
|
|
333
|
+
|
|
334
|
+
return AutomationResult(
|
|
335
|
+
success=True,
|
|
336
|
+
operation="find_all_images_on_screen",
|
|
337
|
+
data={
|
|
338
|
+
"image_path": image_path,
|
|
339
|
+
"matches": matches,
|
|
340
|
+
"match_count": len(matches),
|
|
341
|
+
"region": region,
|
|
342
|
+
"search_confidence": confidence
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
except pyautogui.ImageNotFoundException:
|
|
347
|
+
return AutomationResult(
|
|
348
|
+
success=True,
|
|
349
|
+
operation="find_all_images_on_screen",
|
|
350
|
+
data={
|
|
351
|
+
"image_path": image_path,
|
|
352
|
+
"matches": [],
|
|
353
|
+
"match_count": 0,
|
|
354
|
+
"region": region,
|
|
355
|
+
"search_confidence": confidence
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
logger.error(f"Find all images failed: {e}")
|
|
361
|
+
return AutomationResult(
|
|
362
|
+
success=False,
|
|
363
|
+
operation="find_all_images_on_screen",
|
|
364
|
+
error=str(e)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# ==========================================
|
|
368
|
+
# MOUSE CONTROL
|
|
369
|
+
# ==========================================
|
|
370
|
+
|
|
371
|
+
def get_mouse_position(self) -> AutomationResult:
|
|
372
|
+
"""Get current mouse position"""
|
|
373
|
+
try:
|
|
374
|
+
x, y = pyautogui.position()
|
|
375
|
+
|
|
376
|
+
mouse_info = MouseInfo(x=x, y=y)
|
|
377
|
+
|
|
378
|
+
return AutomationResult(
|
|
379
|
+
success=True,
|
|
380
|
+
operation="get_mouse_position",
|
|
381
|
+
data=mouse_info.__dict__
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
except Exception as e:
|
|
385
|
+
logger.error(f"Get mouse position failed: {e}")
|
|
386
|
+
return AutomationResult(
|
|
387
|
+
success=False,
|
|
388
|
+
operation="get_mouse_position",
|
|
389
|
+
error=str(e)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def move_mouse(self, x: int, y: int, duration: float = 0.5,
|
|
393
|
+
relative: bool = False) -> AutomationResult:
|
|
394
|
+
"""Move mouse to specified coordinates"""
|
|
395
|
+
try:
|
|
396
|
+
if relative:
|
|
397
|
+
# Move relative to current position
|
|
398
|
+
current_x, current_y = pyautogui.position()
|
|
399
|
+
target_x = current_x + x
|
|
400
|
+
target_y = current_y + y
|
|
401
|
+
else:
|
|
402
|
+
target_x, target_y = x, y
|
|
403
|
+
|
|
404
|
+
# Validate target coordinates
|
|
405
|
+
if not (0 <= target_x < self.screen_info.width and 0 <= target_y < self.screen_info.height):
|
|
406
|
+
raise ValueError(f"Target coordinates ({target_x}, {target_y}) are outside screen bounds")
|
|
407
|
+
|
|
408
|
+
# Move mouse
|
|
409
|
+
pyautogui.moveTo(target_x, target_y, duration=duration)
|
|
410
|
+
|
|
411
|
+
return AutomationResult(
|
|
412
|
+
success=True,
|
|
413
|
+
operation="move_mouse",
|
|
414
|
+
data={
|
|
415
|
+
"target_x": target_x,
|
|
416
|
+
"target_y": target_y,
|
|
417
|
+
"duration": duration,
|
|
418
|
+
"relative": relative,
|
|
419
|
+
"original_coords": [x, y] if relative else None
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
except Exception as e:
|
|
424
|
+
logger.error(f"Move mouse failed: {e}")
|
|
425
|
+
return AutomationResult(
|
|
426
|
+
success=False,
|
|
427
|
+
operation="move_mouse",
|
|
428
|
+
error=str(e)
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
def click_mouse(self, x: Optional[int] = None, y: Optional[int] = None,
|
|
432
|
+
button: str = 'left', clicks: int = 1, interval: float = 0.0) -> AutomationResult:
|
|
433
|
+
"""Click mouse at specified coordinates"""
|
|
434
|
+
try:
|
|
435
|
+
# Validate button
|
|
436
|
+
valid_buttons = ['left', 'right', 'middle']
|
|
437
|
+
if button not in valid_buttons:
|
|
438
|
+
raise ValueError(f"Invalid button '{button}'. Must be one of: {valid_buttons}")
|
|
439
|
+
|
|
440
|
+
# Click at coordinates or current position
|
|
441
|
+
if x is not None and y is not None:
|
|
442
|
+
# Validate coordinates
|
|
443
|
+
if not (0 <= x < self.screen_info.width and 0 <= y < self.screen_info.height):
|
|
444
|
+
raise ValueError(f"Coordinates ({x}, {y}) are outside screen bounds")
|
|
445
|
+
|
|
446
|
+
pyautogui.click(x, y, clicks=clicks, interval=interval, button=button)
|
|
447
|
+
click_position = (x, y)
|
|
448
|
+
else:
|
|
449
|
+
pyautogui.click(clicks=clicks, interval=interval, button=button)
|
|
450
|
+
click_position = pyautogui.position()
|
|
451
|
+
|
|
452
|
+
return AutomationResult(
|
|
453
|
+
success=True,
|
|
454
|
+
operation="click_mouse",
|
|
455
|
+
data={
|
|
456
|
+
"position": click_position,
|
|
457
|
+
"button": button,
|
|
458
|
+
"clicks": clicks,
|
|
459
|
+
"interval": interval
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
except Exception as e:
|
|
464
|
+
logger.error(f"Mouse click failed: {e}")
|
|
465
|
+
return AutomationResult(
|
|
466
|
+
success=False,
|
|
467
|
+
operation="click_mouse",
|
|
468
|
+
error=str(e)
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
def drag_mouse(self, start_x: int, start_y: int, end_x: int, end_y: int,
|
|
472
|
+
duration: float = 1.0, button: str = 'left') -> AutomationResult:
|
|
473
|
+
"""Drag mouse from start to end coordinates"""
|
|
474
|
+
try:
|
|
475
|
+
# Validate coordinates
|
|
476
|
+
coords_to_check = [(start_x, start_y), (end_x, end_y)]
|
|
477
|
+
for x, y in coords_to_check:
|
|
478
|
+
if not (0 <= x < self.screen_info.width and 0 <= y < self.screen_info.height):
|
|
479
|
+
raise ValueError(f"Coordinates ({x}, {y}) are outside screen bounds")
|
|
480
|
+
|
|
481
|
+
# Validate button
|
|
482
|
+
valid_buttons = ['left', 'right', 'middle']
|
|
483
|
+
if button not in valid_buttons:
|
|
484
|
+
raise ValueError(f"Invalid button '{button}'. Must be one of: {valid_buttons}")
|
|
485
|
+
|
|
486
|
+
# Perform drag operation
|
|
487
|
+
pyautogui.drag(end_x - start_x, end_y - start_y, duration=duration, button=button)
|
|
488
|
+
|
|
489
|
+
return AutomationResult(
|
|
490
|
+
success=True,
|
|
491
|
+
operation="drag_mouse",
|
|
492
|
+
data={
|
|
493
|
+
"start_position": [start_x, start_y],
|
|
494
|
+
"end_position": [end_x, end_y],
|
|
495
|
+
"duration": duration,
|
|
496
|
+
"button": button,
|
|
497
|
+
"distance": ((end_x - start_x)**2 + (end_y - start_y)**2)**0.5
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
except Exception as e:
|
|
502
|
+
logger.error(f"Mouse drag failed: {e}")
|
|
503
|
+
return AutomationResult(
|
|
504
|
+
success=False,
|
|
505
|
+
operation="drag_mouse",
|
|
506
|
+
error=str(e)
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
def scroll_mouse(self, clicks: int, x: Optional[int] = None, y: Optional[int] = None) -> AutomationResult:
|
|
510
|
+
"""Scroll mouse wheel"""
|
|
511
|
+
try:
|
|
512
|
+
# Scroll at coordinates or current position
|
|
513
|
+
if x is not None and y is not None:
|
|
514
|
+
# Validate coordinates
|
|
515
|
+
if not (0 <= x < self.screen_info.width and 0 <= y < self.screen_info.height):
|
|
516
|
+
raise ValueError(f"Coordinates ({x}, {y}) are outside screen bounds")
|
|
517
|
+
|
|
518
|
+
pyautogui.scroll(clicks, x, y)
|
|
519
|
+
scroll_position = (x, y)
|
|
520
|
+
else:
|
|
521
|
+
pyautogui.scroll(clicks)
|
|
522
|
+
scroll_position = pyautogui.position()
|
|
523
|
+
|
|
524
|
+
return AutomationResult(
|
|
525
|
+
success=True,
|
|
526
|
+
operation="scroll_mouse",
|
|
527
|
+
data={
|
|
528
|
+
"clicks": clicks,
|
|
529
|
+
"position": scroll_position,
|
|
530
|
+
"direction": "up" if clicks > 0 else "down"
|
|
531
|
+
}
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
except Exception as e:
|
|
535
|
+
logger.error(f"Mouse scroll failed: {e}")
|
|
536
|
+
return AutomationResult(
|
|
537
|
+
success=False,
|
|
538
|
+
operation="scroll_mouse",
|
|
539
|
+
error=str(e)
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# ==========================================
|
|
543
|
+
# KEYBOARD CONTROL
|
|
544
|
+
# ==========================================
|
|
545
|
+
|
|
546
|
+
def type_text(self, text: str, interval: float = 0.0) -> AutomationResult:
|
|
547
|
+
"""Type text with specified interval between characters"""
|
|
548
|
+
try:
|
|
549
|
+
pyautogui.typewrite(text, interval=interval)
|
|
550
|
+
|
|
551
|
+
return AutomationResult(
|
|
552
|
+
success=True,
|
|
553
|
+
operation="type_text",
|
|
554
|
+
data={
|
|
555
|
+
"text": text,
|
|
556
|
+
"length": len(text),
|
|
557
|
+
"interval": interval,
|
|
558
|
+
"estimated_duration": len(text) * interval
|
|
559
|
+
}
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
except Exception as e:
|
|
563
|
+
logger.error(f"Type text failed: {e}")
|
|
564
|
+
return AutomationResult(
|
|
565
|
+
success=False,
|
|
566
|
+
operation="type_text",
|
|
567
|
+
error=str(e)
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
def press_key(self, key: str, presses: int = 1, interval: float = 0.0) -> AutomationResult:
|
|
571
|
+
"""Press a key multiple times"""
|
|
572
|
+
try:
|
|
573
|
+
pyautogui.press(key, presses=presses, interval=interval)
|
|
574
|
+
|
|
575
|
+
return AutomationResult(
|
|
576
|
+
success=True,
|
|
577
|
+
operation="press_key",
|
|
578
|
+
data={
|
|
579
|
+
"key": key,
|
|
580
|
+
"presses": presses,
|
|
581
|
+
"interval": interval
|
|
582
|
+
}
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
except Exception as e:
|
|
586
|
+
logger.error(f"Press key failed: {e}")
|
|
587
|
+
return AutomationResult(
|
|
588
|
+
success=False,
|
|
589
|
+
operation="press_key",
|
|
590
|
+
error=str(e)
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
def key_combination(self, keys: List[str]) -> AutomationResult:
|
|
594
|
+
"""Press a combination of keys simultaneously"""
|
|
595
|
+
try:
|
|
596
|
+
pyautogui.hotkey(*keys)
|
|
597
|
+
|
|
598
|
+
return AutomationResult(
|
|
599
|
+
success=True,
|
|
600
|
+
operation="key_combination",
|
|
601
|
+
data={
|
|
602
|
+
"keys": keys,
|
|
603
|
+
"combination": "+".join(keys)
|
|
604
|
+
}
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
except Exception as e:
|
|
608
|
+
logger.error(f"Key combination failed: {e}")
|
|
609
|
+
return AutomationResult(
|
|
610
|
+
success=False,
|
|
611
|
+
operation="key_combination",
|
|
612
|
+
error=str(e)
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
def hold_key(self, key: str, duration: float = 1.0) -> AutomationResult:
|
|
616
|
+
"""Hold a key down for specified duration"""
|
|
617
|
+
try:
|
|
618
|
+
pyautogui.keyDown(key)
|
|
619
|
+
time.sleep(duration)
|
|
620
|
+
pyautogui.keyUp(key)
|
|
621
|
+
|
|
622
|
+
return AutomationResult(
|
|
623
|
+
success=True,
|
|
624
|
+
operation="hold_key",
|
|
625
|
+
data={
|
|
626
|
+
"key": key,
|
|
627
|
+
"duration": duration
|
|
628
|
+
}
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
except Exception as e:
|
|
632
|
+
logger.error(f"Hold key failed: {e}")
|
|
633
|
+
return AutomationResult(
|
|
634
|
+
success=False,
|
|
635
|
+
operation="hold_key",
|
|
636
|
+
error=str(e)
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
# ==========================================
|
|
640
|
+
# UTILITY FUNCTIONS
|
|
641
|
+
# ==========================================
|
|
642
|
+
|
|
643
|
+
def get_screen_info(self) -> AutomationResult:
|
|
644
|
+
"""Get detailed screen information"""
|
|
645
|
+
try:
|
|
646
|
+
return AutomationResult(
|
|
647
|
+
success=True,
|
|
648
|
+
operation="get_screen_info",
|
|
649
|
+
data=self.screen_info.__dict__
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
except Exception as e:
|
|
653
|
+
logger.error(f"Get screen info failed: {e}")
|
|
654
|
+
return AutomationResult(
|
|
655
|
+
success=False,
|
|
656
|
+
operation="get_screen_info",
|
|
657
|
+
error=str(e)
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
def is_on_screen(self, x: int, y: int) -> AutomationResult:
|
|
661
|
+
"""Check if coordinates are on screen"""
|
|
662
|
+
try:
|
|
663
|
+
on_screen = pyautogui.onScreen(x, y)
|
|
664
|
+
|
|
665
|
+
return AutomationResult(
|
|
666
|
+
success=True,
|
|
667
|
+
operation="is_on_screen",
|
|
668
|
+
data={
|
|
669
|
+
"x": x,
|
|
670
|
+
"y": y,
|
|
671
|
+
"on_screen": on_screen,
|
|
672
|
+
"screen_width": self.screen_info.width,
|
|
673
|
+
"screen_height": self.screen_info.height
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
except Exception as e:
|
|
678
|
+
logger.error(f"Is on screen check failed: {e}")
|
|
679
|
+
return AutomationResult(
|
|
680
|
+
success=False,
|
|
681
|
+
operation="is_on_screen",
|
|
682
|
+
error=str(e)
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
def set_pause(self, pause_duration: float) -> AutomationResult:
|
|
686
|
+
"""Set the pause duration between PyAutoGUI actions"""
|
|
687
|
+
try:
|
|
688
|
+
old_pause = pyautogui.PAUSE
|
|
689
|
+
pyautogui.PAUSE = pause_duration
|
|
690
|
+
|
|
691
|
+
return AutomationResult(
|
|
692
|
+
success=True,
|
|
693
|
+
operation="set_pause",
|
|
694
|
+
data={
|
|
695
|
+
"old_pause": old_pause,
|
|
696
|
+
"new_pause": pause_duration
|
|
697
|
+
}
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
except Exception as e:
|
|
701
|
+
logger.error(f"Set pause failed: {e}")
|
|
702
|
+
return AutomationResult(
|
|
703
|
+
success=False,
|
|
704
|
+
operation="set_pause",
|
|
705
|
+
error=str(e)
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
def set_failsafe(self, enabled: bool) -> AutomationResult:
|
|
709
|
+
"""Enable or disable PyAutoGUI failsafe"""
|
|
710
|
+
try:
|
|
711
|
+
old_failsafe = pyautogui.FAILSAFE
|
|
712
|
+
pyautogui.FAILSAFE = enabled
|
|
713
|
+
|
|
714
|
+
return AutomationResult(
|
|
715
|
+
success=True,
|
|
716
|
+
operation="set_failsafe",
|
|
717
|
+
data={
|
|
718
|
+
"old_failsafe": old_failsafe,
|
|
719
|
+
"new_failsafe": enabled
|
|
720
|
+
}
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
except Exception as e:
|
|
724
|
+
logger.error(f"Set failsafe failed: {e}")
|
|
725
|
+
return AutomationResult(
|
|
726
|
+
success=False,
|
|
727
|
+
operation="set_failsafe",
|
|
728
|
+
error=str(e)
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
# ==========================================
|
|
732
|
+
# ADVANCED FEATURES
|
|
733
|
+
# ==========================================
|
|
734
|
+
|
|
735
|
+
def create_image_template(self, name: str, x: int, y: int, width: int, height: int) -> AutomationResult:
|
|
736
|
+
"""Create an image template from screen region for future recognition"""
|
|
737
|
+
try:
|
|
738
|
+
# Take screenshot of region
|
|
739
|
+
region = (x, y, width, height)
|
|
740
|
+
template_img = pyautogui.screenshot(region=region)
|
|
741
|
+
|
|
742
|
+
# Save template
|
|
743
|
+
self.image_templates[name] = {
|
|
744
|
+
'image': template_img,
|
|
745
|
+
'region': region,
|
|
746
|
+
'created_at': time.time()
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
# Convert to base64 for return
|
|
750
|
+
img_buffer = io.BytesIO()
|
|
751
|
+
template_img.save(img_buffer, format='PNG')
|
|
752
|
+
img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
|
|
753
|
+
|
|
754
|
+
return AutomationResult(
|
|
755
|
+
success=True,
|
|
756
|
+
operation="create_image_template",
|
|
757
|
+
data={
|
|
758
|
+
"template_name": name,
|
|
759
|
+
"region": region,
|
|
760
|
+
"image_base64": img_base64,
|
|
761
|
+
"width": template_img.width,
|
|
762
|
+
"height": template_img.height
|
|
763
|
+
}
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
except Exception as e:
|
|
767
|
+
logger.error(f"Create image template failed: {e}")
|
|
768
|
+
return AutomationResult(
|
|
769
|
+
success=False,
|
|
770
|
+
operation="create_image_template",
|
|
771
|
+
error=str(e)
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
def find_template_on_screen(self, template_name: str, confidence: float = 0.8) -> AutomationResult:
|
|
775
|
+
"""Find a previously created template on the screen"""
|
|
776
|
+
try:
|
|
777
|
+
if template_name not in self.image_templates:
|
|
778
|
+
raise ValueError(f"Template '{template_name}' not found")
|
|
779
|
+
|
|
780
|
+
template_data = self.image_templates[template_name]
|
|
781
|
+
template_img = template_data['image']
|
|
782
|
+
|
|
783
|
+
# Save template temporarily for locateOnScreen
|
|
784
|
+
temp_path = f"temp_template_{template_name}.png"
|
|
785
|
+
template_img.save(temp_path)
|
|
786
|
+
|
|
787
|
+
try:
|
|
788
|
+
# Search for template
|
|
789
|
+
location = pyautogui.locateOnScreen(temp_path, confidence=confidence)
|
|
790
|
+
|
|
791
|
+
if location:
|
|
792
|
+
center = pyautogui.center(location)
|
|
793
|
+
match = ImageMatch(
|
|
794
|
+
found=True,
|
|
795
|
+
x=location.left,
|
|
796
|
+
y=location.top,
|
|
797
|
+
width=location.width,
|
|
798
|
+
height=location.height,
|
|
799
|
+
confidence=confidence,
|
|
800
|
+
center_x=center.x,
|
|
801
|
+
center_y=center.y
|
|
802
|
+
)
|
|
803
|
+
else:
|
|
804
|
+
match = ImageMatch(found=False)
|
|
805
|
+
|
|
806
|
+
return AutomationResult(
|
|
807
|
+
success=True,
|
|
808
|
+
operation="find_template_on_screen",
|
|
809
|
+
data={
|
|
810
|
+
"template_name": template_name,
|
|
811
|
+
"match": match.__dict__,
|
|
812
|
+
"search_confidence": confidence
|
|
813
|
+
}
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
finally:
|
|
817
|
+
# Clean up temporary file
|
|
818
|
+
if os.path.exists(temp_path):
|
|
819
|
+
os.remove(temp_path)
|
|
820
|
+
|
|
821
|
+
except Exception as e:
|
|
822
|
+
logger.error(f"Find template failed: {e}")
|
|
823
|
+
return AutomationResult(
|
|
824
|
+
success=False,
|
|
825
|
+
operation="find_template_on_screen",
|
|
826
|
+
error=str(e)
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
def get_available_keys(self) -> AutomationResult:
|
|
830
|
+
"""Get list of all available keyboard keys"""
|
|
831
|
+
try:
|
|
832
|
+
# PyAutoGUI key names
|
|
833
|
+
available_keys = [
|
|
834
|
+
# Letters
|
|
835
|
+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
|
836
|
+
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
|
837
|
+
|
|
838
|
+
# Numbers
|
|
839
|
+
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
|
840
|
+
|
|
841
|
+
# Function keys
|
|
842
|
+
'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12',
|
|
843
|
+
|
|
844
|
+
# Special keys
|
|
845
|
+
'enter', 'return', 'space', 'tab', 'backspace', 'delete', 'esc', 'escape',
|
|
846
|
+
'shift', 'ctrl', 'alt', 'win', 'cmd', 'option',
|
|
847
|
+
|
|
848
|
+
# Arrow keys
|
|
849
|
+
'up', 'down', 'left', 'right',
|
|
850
|
+
|
|
851
|
+
# Navigation
|
|
852
|
+
'home', 'end', 'pageup', 'pagedown', 'insert',
|
|
853
|
+
|
|
854
|
+
# Symbols
|
|
855
|
+
'!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '=', '+',
|
|
856
|
+
'[', ']', '{', '}', '\\', '|', ';', ':', "'", '"', ',', '.', '<', '>',
|
|
857
|
+
'/', '?', '`', '~',
|
|
858
|
+
|
|
859
|
+
# Lock keys
|
|
860
|
+
'capslock', 'numlock', 'scrolllock',
|
|
861
|
+
|
|
862
|
+
# Numpad
|
|
863
|
+
'num0', 'num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7', 'num8', 'num9',
|
|
864
|
+
'add', 'subtract', 'multiply', 'divide', 'decimal'
|
|
865
|
+
]
|
|
866
|
+
|
|
867
|
+
# Organize by category
|
|
868
|
+
key_categories = {
|
|
869
|
+
'letters': [k for k in available_keys if k.isalpha() and len(k) == 1],
|
|
870
|
+
'numbers': [k for k in available_keys if k.isdigit()],
|
|
871
|
+
'function_keys': [k for k in available_keys if k.startswith('f') and k[1:].isdigit()],
|
|
872
|
+
'arrow_keys': ['up', 'down', 'left', 'right'],
|
|
873
|
+
'modifier_keys': ['shift', 'ctrl', 'alt', 'win', 'cmd', 'option'],
|
|
874
|
+
'navigation_keys': ['home', 'end', 'pageup', 'pagedown', 'insert'],
|
|
875
|
+
'special_keys': ['enter', 'return', 'space', 'tab', 'backspace', 'delete', 'esc', 'escape'],
|
|
876
|
+
'lock_keys': ['capslock', 'numlock', 'scrolllock'],
|
|
877
|
+
'numpad_keys': [k for k in available_keys if k.startswith('num') or k in ['add', 'subtract', 'multiply', 'divide', 'decimal']],
|
|
878
|
+
'symbols': [k for k in available_keys if not k.isalnum() and len(k) == 1]
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return AutomationResult(
|
|
882
|
+
success=True,
|
|
883
|
+
operation="get_available_keys",
|
|
884
|
+
data={
|
|
885
|
+
"all_keys": available_keys,
|
|
886
|
+
"categories": key_categories,
|
|
887
|
+
"total_count": len(available_keys)
|
|
888
|
+
}
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
except Exception as e:
|
|
892
|
+
logger.error(f"Get available keys failed: {e}")
|
|
893
|
+
return AutomationResult(
|
|
894
|
+
success=False,
|
|
895
|
+
operation="get_available_keys",
|
|
896
|
+
error=str(e)
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
# Singleton instance for global access
|
|
900
|
+
_pyautogui_controller = None
|
|
901
|
+
|
|
902
|
+
def get_pyautogui_controller() -> PyAutoGUIController:
|
|
903
|
+
"""Get the global PyAutoGUI controller instance"""
|
|
904
|
+
global _pyautogui_controller
|
|
905
|
+
if _pyautogui_controller is None:
|
|
906
|
+
_pyautogui_controller = PyAutoGUIController()
|
|
907
|
+
return _pyautogui_controller
|
|
908
|
+
|
|
909
|
+
# Convenience functions for direct access
|
|
910
|
+
def screenshot(region=None, save_path=None):
|
|
911
|
+
"""Take a screenshot"""
|
|
912
|
+
return get_pyautogui_controller().take_screenshot(region, save_path)
|
|
913
|
+
|
|
914
|
+
def pixel(x, y):
|
|
915
|
+
"""Get pixel color"""
|
|
916
|
+
return get_pyautogui_controller().get_pixel_color(x, y)
|
|
917
|
+
|
|
918
|
+
def locate_image(image_path, confidence=0.8, region=None):
|
|
919
|
+
"""Find image on screen"""
|
|
920
|
+
return get_pyautogui_controller().find_image_on_screen(image_path, confidence, region)
|
|
921
|
+
|
|
922
|
+
def mouse_position():
|
|
923
|
+
"""Get mouse position"""
|
|
924
|
+
return get_pyautogui_controller().get_mouse_position()
|
|
925
|
+
|
|
926
|
+
def click(x=None, y=None, button='left', clicks=1):
|
|
927
|
+
"""Click mouse"""
|
|
928
|
+
return get_pyautogui_controller().click_mouse(x, y, button, clicks)
|
|
929
|
+
|
|
930
|
+
def type_text(text, interval=0.0):
|
|
931
|
+
"""Type text"""
|
|
932
|
+
return get_pyautogui_controller().type_text(text, interval)
|
|
933
|
+
|
|
934
|
+
def press(key, presses=1):
|
|
935
|
+
"""Press key"""
|
|
936
|
+
return get_pyautogui_controller().press_key(key, presses)
|
|
937
|
+
|
|
938
|
+
if __name__ == "__main__":
|
|
939
|
+
# Quick test
|
|
940
|
+
if PYAUTOGUI_AVAILABLE:
|
|
941
|
+
controller = PyAutoGUIController()
|
|
942
|
+
print("PyAutoGUI Controller initialized successfully!")
|
|
943
|
+
|
|
944
|
+
# Test basic functionality
|
|
945
|
+
screen_info = controller.get_screen_info()
|
|
946
|
+
print(f"Screen: {screen_info.data}")
|
|
947
|
+
|
|
948
|
+
mouse_pos = controller.get_mouse_position()
|
|
949
|
+
print(f"Mouse: {mouse_pos.data}")
|
|
950
|
+
else:
|
|
951
|
+
print("PyAutoGUI not available. Install with: pip install pyautogui pillow opencv-python")
|