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,368 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data Types Module
|
|
3
|
+
Common data type definitions and utilities
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Optional, Dict, Any, List, Union
|
|
8
|
+
from enum import Enum
|
|
9
|
+
import struct
|
|
10
|
+
|
|
11
|
+
class DataType(Enum):
|
|
12
|
+
"""Enumeration of supported data types"""
|
|
13
|
+
UINT8 = "uint8"
|
|
14
|
+
INT8 = "int8"
|
|
15
|
+
UINT16 = "uint16"
|
|
16
|
+
INT16 = "int16"
|
|
17
|
+
UINT32 = "uint32"
|
|
18
|
+
INT32 = "int32"
|
|
19
|
+
UINT64 = "uint64"
|
|
20
|
+
INT64 = "int64"
|
|
21
|
+
FLOAT = "float"
|
|
22
|
+
DOUBLE = "double"
|
|
23
|
+
STRING = "string"
|
|
24
|
+
POINTER = "pointer"
|
|
25
|
+
BYTES = "bytes"
|
|
26
|
+
|
|
27
|
+
class Architecture(Enum):
|
|
28
|
+
"""Target architecture enumeration"""
|
|
29
|
+
X86 = "x86"
|
|
30
|
+
X64 = "x64"
|
|
31
|
+
UNKNOWN = "unknown"
|
|
32
|
+
|
|
33
|
+
class MemoryProtection(Enum):
|
|
34
|
+
"""Memory protection flags"""
|
|
35
|
+
NOACCESS = 0x01
|
|
36
|
+
READONLY = 0x02
|
|
37
|
+
READWRITE = 0x04
|
|
38
|
+
WRITECOPY = 0x08
|
|
39
|
+
EXECUTE = 0x10
|
|
40
|
+
EXECUTE_READ = 0x20
|
|
41
|
+
EXECUTE_READWRITE = 0x40
|
|
42
|
+
EXECUTE_WRITECOPY = 0x80
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ProcessSnapshot:
|
|
46
|
+
"""Snapshot of process state"""
|
|
47
|
+
pid: int
|
|
48
|
+
name: str
|
|
49
|
+
timestamp: float
|
|
50
|
+
memory_usage: int
|
|
51
|
+
thread_count: int
|
|
52
|
+
handle_count: Optional[int] = None
|
|
53
|
+
cpu_usage: Optional[float] = None
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class MemoryBlock:
|
|
57
|
+
"""Represents a block of memory"""
|
|
58
|
+
address: int
|
|
59
|
+
size: int
|
|
60
|
+
data: bytes
|
|
61
|
+
protection: int
|
|
62
|
+
timestamp: float
|
|
63
|
+
|
|
64
|
+
def contains_address(self, addr: int) -> bool:
|
|
65
|
+
"""Check if this block contains the given address"""
|
|
66
|
+
return self.address <= addr < self.address + self.size
|
|
67
|
+
|
|
68
|
+
def get_offset(self, addr: int) -> Optional[int]:
|
|
69
|
+
"""Get offset of address within this block"""
|
|
70
|
+
if self.contains_address(addr):
|
|
71
|
+
return addr - self.address
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class ScanResult:
|
|
76
|
+
"""Result of a memory scan operation"""
|
|
77
|
+
address: int
|
|
78
|
+
value: Any
|
|
79
|
+
data_type: DataType
|
|
80
|
+
context: Optional[bytes] = None
|
|
81
|
+
confidence: float = 1.0
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class PointerChain:
|
|
85
|
+
"""Represents a chain of pointers"""
|
|
86
|
+
base_address: int
|
|
87
|
+
offsets: List[int]
|
|
88
|
+
final_address: int
|
|
89
|
+
valid: bool = True
|
|
90
|
+
|
|
91
|
+
def __str__(self) -> str:
|
|
92
|
+
"""String representation of pointer chain"""
|
|
93
|
+
if not self.offsets:
|
|
94
|
+
return f"0x{self.base_address:08X}"
|
|
95
|
+
|
|
96
|
+
offset_str = " + ".join(f"0x{offset:X}" for offset in self.offsets)
|
|
97
|
+
return f"[0x{self.base_address:08X} + {offset_str}] -> 0x{self.final_address:08X}"
|
|
98
|
+
|
|
99
|
+
class DataTypeConverter:
|
|
100
|
+
"""Utility class for data type conversions"""
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def bytes_to_value(data: bytes, data_type: DataType, offset: int = 0) -> Any:
|
|
104
|
+
"""Convert bytes to a specific data type"""
|
|
105
|
+
if offset + DataTypeConverter.get_size(data_type) > len(data):
|
|
106
|
+
raise ValueError("Insufficient data for conversion")
|
|
107
|
+
|
|
108
|
+
chunk = data[offset:offset + DataTypeConverter.get_size(data_type)]
|
|
109
|
+
|
|
110
|
+
if data_type == DataType.UINT8:
|
|
111
|
+
return chunk[0]
|
|
112
|
+
elif data_type == DataType.INT8:
|
|
113
|
+
return struct.unpack('<b', chunk)[0]
|
|
114
|
+
elif data_type == DataType.UINT16:
|
|
115
|
+
return struct.unpack('<H', chunk)[0]
|
|
116
|
+
elif data_type == DataType.INT16:
|
|
117
|
+
return struct.unpack('<h', chunk)[0]
|
|
118
|
+
elif data_type == DataType.UINT32:
|
|
119
|
+
return struct.unpack('<I', chunk)[0]
|
|
120
|
+
elif data_type == DataType.INT32:
|
|
121
|
+
return struct.unpack('<i', chunk)[0]
|
|
122
|
+
elif data_type == DataType.UINT64:
|
|
123
|
+
return struct.unpack('<Q', chunk)[0]
|
|
124
|
+
elif data_type == DataType.INT64:
|
|
125
|
+
return struct.unpack('<q', chunk)[0]
|
|
126
|
+
elif data_type == DataType.FLOAT:
|
|
127
|
+
return struct.unpack('<f', chunk)[0]
|
|
128
|
+
elif data_type == DataType.DOUBLE:
|
|
129
|
+
return struct.unpack('<d', chunk)[0]
|
|
130
|
+
elif data_type == DataType.STRING:
|
|
131
|
+
# Find null terminator
|
|
132
|
+
null_pos = chunk.find(b'\x00')
|
|
133
|
+
if null_pos != -1:
|
|
134
|
+
chunk = chunk[:null_pos]
|
|
135
|
+
return chunk.decode('utf-8', errors='ignore')
|
|
136
|
+
elif data_type == DataType.BYTES:
|
|
137
|
+
return chunk
|
|
138
|
+
else:
|
|
139
|
+
raise ValueError(f"Unsupported data type: {data_type}")
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def value_to_bytes(value: Any, data_type: DataType) -> bytes:
|
|
143
|
+
"""Convert a value to bytes"""
|
|
144
|
+
if data_type == DataType.UINT8:
|
|
145
|
+
return struct.pack('<B', int(value))
|
|
146
|
+
elif data_type == DataType.INT8:
|
|
147
|
+
return struct.pack('<b', int(value))
|
|
148
|
+
elif data_type == DataType.UINT16:
|
|
149
|
+
return struct.pack('<H', int(value))
|
|
150
|
+
elif data_type == DataType.INT16:
|
|
151
|
+
return struct.pack('<h', int(value))
|
|
152
|
+
elif data_type == DataType.UINT32:
|
|
153
|
+
return struct.pack('<I', int(value))
|
|
154
|
+
elif data_type == DataType.INT32:
|
|
155
|
+
return struct.pack('<i', int(value))
|
|
156
|
+
elif data_type == DataType.UINT64:
|
|
157
|
+
return struct.pack('<Q', int(value))
|
|
158
|
+
elif data_type == DataType.INT64:
|
|
159
|
+
return struct.pack('<q', int(value))
|
|
160
|
+
elif data_type == DataType.FLOAT:
|
|
161
|
+
return struct.pack('<f', float(value))
|
|
162
|
+
elif data_type == DataType.DOUBLE:
|
|
163
|
+
return struct.pack('<d', float(value))
|
|
164
|
+
elif data_type == DataType.STRING:
|
|
165
|
+
return str(value).encode('utf-8') + b'\x00'
|
|
166
|
+
elif data_type == DataType.BYTES:
|
|
167
|
+
return bytes(value)
|
|
168
|
+
else:
|
|
169
|
+
raise ValueError(f"Unsupported data type: {data_type}")
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def get_size(data_type: DataType) -> int:
|
|
173
|
+
"""Get the size in bytes of a data type"""
|
|
174
|
+
sizes = {
|
|
175
|
+
DataType.UINT8: 1,
|
|
176
|
+
DataType.INT8: 1,
|
|
177
|
+
DataType.UINT16: 2,
|
|
178
|
+
DataType.INT16: 2,
|
|
179
|
+
DataType.UINT32: 4,
|
|
180
|
+
DataType.INT32: 4,
|
|
181
|
+
DataType.UINT64: 8,
|
|
182
|
+
DataType.INT64: 8,
|
|
183
|
+
DataType.FLOAT: 4,
|
|
184
|
+
DataType.DOUBLE: 8,
|
|
185
|
+
DataType.POINTER: 8, # Default to 64-bit
|
|
186
|
+
DataType.STRING: -1, # Variable length
|
|
187
|
+
DataType.BYTES: -1, # Variable length
|
|
188
|
+
}
|
|
189
|
+
return sizes.get(data_type, -1)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def get_pointer_size(architecture: Architecture) -> int:
|
|
193
|
+
"""Get pointer size for architecture"""
|
|
194
|
+
if architecture == Architecture.X64:
|
|
195
|
+
return 8
|
|
196
|
+
elif architecture == Architecture.X86:
|
|
197
|
+
return 4
|
|
198
|
+
else:
|
|
199
|
+
return 8 # Default to 64-bit
|
|
200
|
+
|
|
201
|
+
class PatternMatcher:
|
|
202
|
+
"""Utility for pattern matching operations"""
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def parse_pattern(pattern: str) -> tuple[bytes, bytes]:
|
|
206
|
+
"""Parse a pattern string into pattern and mask bytes
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
pattern: Pattern string like "48 8B 05 ?? ?? ?? ??"
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Tuple of (pattern_bytes, mask_bytes)
|
|
213
|
+
"""
|
|
214
|
+
import re
|
|
215
|
+
|
|
216
|
+
# Clean up the pattern
|
|
217
|
+
pattern = re.sub(r'\s+', ' ', pattern.strip().upper())
|
|
218
|
+
parts = pattern.split(' ')
|
|
219
|
+
|
|
220
|
+
pattern_bytes = bytearray()
|
|
221
|
+
mask_bytes = bytearray()
|
|
222
|
+
|
|
223
|
+
for part in parts:
|
|
224
|
+
if part in ['??', '?']:
|
|
225
|
+
pattern_bytes.append(0x00)
|
|
226
|
+
mask_bytes.append(0x00)
|
|
227
|
+
else:
|
|
228
|
+
try:
|
|
229
|
+
byte_val = int(part, 16)
|
|
230
|
+
pattern_bytes.append(byte_val)
|
|
231
|
+
mask_bytes.append(0xFF)
|
|
232
|
+
except ValueError:
|
|
233
|
+
raise ValueError(f"Invalid byte in pattern: {part}")
|
|
234
|
+
|
|
235
|
+
return bytes(pattern_bytes), bytes(mask_bytes)
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def match_pattern(data: bytes, pattern: bytes, mask: bytes, offset: int = 0) -> bool:
|
|
239
|
+
"""Check if pattern matches at given offset"""
|
|
240
|
+
if offset + len(pattern) > len(data):
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
for i in range(len(pattern)):
|
|
244
|
+
if mask[i] == 0xFF: # Must match exactly
|
|
245
|
+
if data[offset + i] != pattern[i]:
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
return True
|
|
249
|
+
|
|
250
|
+
class AddressCalculator:
|
|
251
|
+
"""Utility for address calculations"""
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def align_address(address: int, alignment: int) -> int:
|
|
255
|
+
"""Align address to specified boundary"""
|
|
256
|
+
return (address + alignment - 1) & ~(alignment - 1)
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def is_aligned(address: int, alignment: int) -> bool:
|
|
260
|
+
"""Check if address is aligned to boundary"""
|
|
261
|
+
return address % alignment == 0
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def calculate_rva(address: int, base_address: int) -> int:
|
|
265
|
+
"""Calculate relative virtual address"""
|
|
266
|
+
return address - base_address
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def is_valid_user_address(address: int, architecture: Architecture) -> bool:
|
|
270
|
+
"""Check if address is in valid user space range"""
|
|
271
|
+
if architecture == Architecture.X86:
|
|
272
|
+
return 0x00010000 <= address <= 0x7FFFFFFF
|
|
273
|
+
elif architecture == Architecture.X64:
|
|
274
|
+
return 0x0000000000010000 <= address <= 0x00007FFFFFFFFFFF
|
|
275
|
+
else:
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def is_valid_kernel_address(address: int, architecture: Architecture) -> bool:
|
|
280
|
+
"""Check if address is in kernel space range"""
|
|
281
|
+
if architecture == Architecture.X86:
|
|
282
|
+
return 0x80000000 <= address <= 0xFFFFFFFF
|
|
283
|
+
elif architecture == Architecture.X64:
|
|
284
|
+
return 0xFFFF800000000000 <= address <= 0xFFFFFFFFFFFFFFFF
|
|
285
|
+
else:
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
@dataclass
|
|
289
|
+
class AnalysisContext:
|
|
290
|
+
"""Context information for analysis operations"""
|
|
291
|
+
target_process: Optional[Dict[str, Any]] = None
|
|
292
|
+
architecture: Architecture = Architecture.UNKNOWN
|
|
293
|
+
base_address: int = 0
|
|
294
|
+
analysis_depth: int = 1
|
|
295
|
+
include_symbols: bool = True
|
|
296
|
+
include_strings: bool = True
|
|
297
|
+
max_results: int = 1000
|
|
298
|
+
|
|
299
|
+
def get_pointer_size(self) -> int:
|
|
300
|
+
"""Get pointer size for current architecture"""
|
|
301
|
+
return DataTypeConverter.get_pointer_size(self.architecture)
|
|
302
|
+
|
|
303
|
+
class CacheEntry:
|
|
304
|
+
"""Cache entry for analysis results"""
|
|
305
|
+
|
|
306
|
+
def __init__(self, key: str, value: Any, ttl: float = 300.0):
|
|
307
|
+
import time
|
|
308
|
+
self.key = key
|
|
309
|
+
self.value = value
|
|
310
|
+
self.created_time = time.time()
|
|
311
|
+
self.ttl = ttl
|
|
312
|
+
|
|
313
|
+
def is_expired(self) -> bool:
|
|
314
|
+
"""Check if cache entry has expired"""
|
|
315
|
+
import time
|
|
316
|
+
return time.time() - self.created_time > self.ttl
|
|
317
|
+
|
|
318
|
+
def refresh(self):
|
|
319
|
+
"""Refresh the cache entry timestamp"""
|
|
320
|
+
import time
|
|
321
|
+
self.created_time = time.time()
|
|
322
|
+
|
|
323
|
+
class SimpleCache:
|
|
324
|
+
"""Simple in-memory cache for analysis results"""
|
|
325
|
+
|
|
326
|
+
def __init__(self, max_size: int = 1000):
|
|
327
|
+
self.cache: Dict[str, CacheEntry] = {}
|
|
328
|
+
self.max_size = max_size
|
|
329
|
+
|
|
330
|
+
def get(self, key: str) -> Optional[Any]:
|
|
331
|
+
"""Get value from cache"""
|
|
332
|
+
if key in self.cache:
|
|
333
|
+
entry = self.cache[key]
|
|
334
|
+
if not entry.is_expired():
|
|
335
|
+
entry.refresh()
|
|
336
|
+
return entry.value
|
|
337
|
+
else:
|
|
338
|
+
del self.cache[key]
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
def put(self, key: str, value: Any, ttl: float = 300.0):
|
|
342
|
+
"""Put value in cache"""
|
|
343
|
+
# Clean up expired entries
|
|
344
|
+
self._cleanup_expired()
|
|
345
|
+
|
|
346
|
+
# Remove oldest entries if at capacity
|
|
347
|
+
if len(self.cache) >= self.max_size:
|
|
348
|
+
# Remove 10% of oldest entries
|
|
349
|
+
oldest_keys = sorted(self.cache.keys(),
|
|
350
|
+
key=lambda k: self.cache[k].created_time)
|
|
351
|
+
for old_key in oldest_keys[:self.max_size // 10]:
|
|
352
|
+
del self.cache[old_key]
|
|
353
|
+
|
|
354
|
+
self.cache[key] = CacheEntry(key, value, ttl)
|
|
355
|
+
|
|
356
|
+
def _cleanup_expired(self):
|
|
357
|
+
"""Remove expired cache entries"""
|
|
358
|
+
expired_keys = [k for k, v in self.cache.items() if v.is_expired()]
|
|
359
|
+
for key in expired_keys:
|
|
360
|
+
del self.cache[key]
|
|
361
|
+
|
|
362
|
+
def clear(self):
|
|
363
|
+
"""Clear all cache entries"""
|
|
364
|
+
self.cache.clear()
|
|
365
|
+
|
|
366
|
+
def size(self) -> int:
|
|
367
|
+
"""Get current cache size"""
|
|
368
|
+
return len(self.cache)
|