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.
Files changed (40) hide show
  1. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/METADATA +16 -0
  2. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/RECORD +40 -0
  3. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/WHEEL +5 -0
  4. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/top_level.txt +1 -0
  7. server/cheatengine/__init__.py +19 -0
  8. server/cheatengine/ce_bridge.py +1670 -0
  9. server/cheatengine/lua_interface.py +460 -0
  10. server/cheatengine/table_parser.py +1221 -0
  11. server/config/__init__.py +20 -0
  12. server/config/settings.py +347 -0
  13. server/config/whitelist.py +378 -0
  14. server/gui_automation/__init__.py +43 -0
  15. server/gui_automation/core/__init__.py +8 -0
  16. server/gui_automation/core/integration.py +951 -0
  17. server/gui_automation/demos/__init__.py +8 -0
  18. server/gui_automation/demos/basic_demo.py +754 -0
  19. server/gui_automation/demos/notepad_demo.py +460 -0
  20. server/gui_automation/demos/simple_demo.py +319 -0
  21. server/gui_automation/tools/__init__.py +8 -0
  22. server/gui_automation/tools/mcp_tools.py +974 -0
  23. server/main.py +519 -0
  24. server/memory/__init__.py +0 -0
  25. server/memory/analyzer.py +0 -0
  26. server/memory/reader.py +0 -0
  27. server/memory/scanner.py +0 -0
  28. server/memory/symbols.py +0 -0
  29. server/process/__init__.py +16 -0
  30. server/process/launcher.py +608 -0
  31. server/process/manager.py +185 -0
  32. server/process/monitors.py +202 -0
  33. server/process/permissions.py +131 -0
  34. server/process_whitelist.json +119 -0
  35. server/pyautogui/__init__.py +0 -0
  36. server/utils/__init__.py +37 -0
  37. server/utils/data_types.py +368 -0
  38. server/utils/formatters.py +430 -0
  39. server/utils/validators.py +340 -0
  40. server/window_automation/__init__.py +59 -0
@@ -0,0 +1,1221 @@
1
+ """
2
+ Cheat Engine Table Parser
3
+ Handles reading, writing, and parsing .CT (Cheat Table) files
4
+ Supports backup creation and comprehensive file operations
5
+ """
6
+
7
+ import struct
8
+ import logging
9
+ import shutil
10
+ import time
11
+ from typing import Dict, List, Optional, Any, BinaryIO
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ import xml.etree.ElementTree as ET
15
+ from datetime import datetime
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ @dataclass
20
+ class CheatEntry:
21
+ """Represents a single cheat entry"""
22
+ id: str
23
+ description: str
24
+ address: Optional[int] = None
25
+ address_string: Optional[str] = None # For module+offset addresses like "D2GAME.dll+1107B8"
26
+ offsets: List[int] = None
27
+ variable_type: str = "4 Bytes"
28
+ value: Any = None
29
+ enabled: bool = False
30
+ hotkey: Optional[str] = None
31
+ script: Optional[str] = None
32
+ group_header: bool = False
33
+
34
+ def __post_init__(self):
35
+ if self.offsets is None:
36
+ self.offsets = []
37
+
38
+ @dataclass
39
+ class StructureElement:
40
+ """Represents a single element in a structure"""
41
+ offset: int
42
+ vartype: str
43
+ bytesize: int
44
+ description: str
45
+ display_method: str = "unsigned integer"
46
+ child_struct: Optional[str] = None
47
+
48
+ @dataclass
49
+ class Structure:
50
+ """Represents a cheat engine structure definition"""
51
+ name: str
52
+ auto_fill: bool = False
53
+ auto_create: bool = True
54
+ default_hex: bool = False
55
+ auto_destroy: bool = False
56
+ elements: List[StructureElement] = None
57
+
58
+ def __post_init__(self):
59
+ if self.elements is None:
60
+ self.elements = []
61
+
62
+ @dataclass
63
+ class DisassemblerComment:
64
+ """Represents a disassembler comment"""
65
+ address: str
66
+ comment: str
67
+
68
+ @dataclass
69
+ class CheatTable:
70
+ """Represents a complete cheat table with all components preserved"""
71
+ title: str = "Cheat Table"
72
+ target_process: str = ""
73
+ entries: List[CheatEntry] = None
74
+ structures: List[Structure] = None
75
+ lua_script: Optional[str] = None
76
+ disassembler_comments: List[DisassemblerComment] = None
77
+ # Preserve the original XML tree for exact reconstruction
78
+ _original_xml_root: Optional[ET.Element] = None
79
+ _original_xml_content: Optional[str] = None
80
+
81
+ def __post_init__(self):
82
+ if self.entries is None:
83
+ self.entries = []
84
+ if self.structures is None:
85
+ self.structures = []
86
+ if self.disassembler_comments is None:
87
+ self.disassembler_comments = []
88
+
89
+ @dataclass
90
+ class AddressList:
91
+ """Collection of cheat entries and enhanced data"""
92
+ entries: List[CheatEntry]
93
+ title: str = "Cheat Table"
94
+ target_process: str = ""
95
+ structures: List[Structure] = None
96
+ lua_script: Optional[str] = None
97
+ disassembler_comments: List[DisassemblerComment] = None
98
+
99
+ def __post_init__(self):
100
+ if self.entries is None:
101
+ self.entries = []
102
+ if self.structures is None:
103
+ self.structures = []
104
+ if self.disassembler_comments is None:
105
+ self.disassembler_comments = []
106
+
107
+ class CheatTableParser:
108
+ """Parser for Cheat Engine .CT files with complete XML preservation"""
109
+
110
+ def __init__(self):
111
+ self.type_map = {
112
+ "Binary": "binary",
113
+ "Byte": "byte",
114
+ "2 Bytes": "word",
115
+ "4 Bytes": "dword",
116
+ "8 Bytes": "qword",
117
+ "Float": "float",
118
+ "Double": "double",
119
+ "String": "string",
120
+ "Array of byte": "aob",
121
+ }
122
+
123
+ def parse_file(self, file_path: str) -> Optional['CheatTable']:
124
+ """
125
+ Parse a .CT file and return a complete CheatTable object
126
+ Preserves all original XML structure for exact reconstruction
127
+
128
+ Args:
129
+ file_path: Path to the .CT file
130
+
131
+ Returns:
132
+ CheatTable object or None if parsing failed
133
+ """
134
+ try:
135
+ if not Path(file_path).exists():
136
+ logger.error(f"Cheat table file not found: {file_path}")
137
+ return None
138
+
139
+ # Try to determine if it's binary or XML format
140
+ with open(file_path, 'rb') as f:
141
+ header = f.read(1024) # Read more data for better detection
142
+ f.seek(0)
143
+
144
+ # Check for XML indicators
145
+ if (b'<?xml' in header or
146
+ b'<CheatTable' in header or
147
+ b'<CheatEntries' in header):
148
+ # XML format (newer CE versions)
149
+ return self._parse_xml_format(f)
150
+ else:
151
+ # Binary format (older CE versions)
152
+ return self._parse_binary_format_to_cheattable(f)
153
+
154
+ except Exception as e:
155
+ logger.error(f"Error parsing cheat table {file_path}: {e}")
156
+ return None
157
+
158
+ def _parse_xml_format(self, file_handle: BinaryIO) -> Optional['CheatTable']:
159
+ """Parse XML format .CT file and preserve complete structure"""
160
+ try:
161
+ # Read the entire file as text for XML parsing
162
+ file_handle.seek(0)
163
+ content = file_handle.read().decode('utf-8', errors='ignore')
164
+
165
+ # Parse XML
166
+ root = ET.fromstring(content)
167
+
168
+ # Find CheatTable root element
169
+ if root.tag != 'CheatTable':
170
+ cheat_table = root.find('.//CheatTable')
171
+ if cheat_table is None:
172
+ logger.error("No CheatTable element found")
173
+ return None
174
+ root = cheat_table
175
+
176
+ # Create CheatTable object and preserve original XML
177
+ cheat_table = CheatTable()
178
+ cheat_table._original_xml_root = root
179
+ cheat_table._original_xml_content = content
180
+
181
+ # Parse header information
182
+ title = "Cheat Table"
183
+ target_process = ""
184
+
185
+ # Look for table info
186
+ for child in root:
187
+ if child.tag == 'CheatTableInfo':
188
+ title_elem = child.find('Title')
189
+ if title_elem is not None and title_elem.text:
190
+ title = title_elem.text
191
+
192
+ elif child.tag == 'Options':
193
+ # Parse options if available
194
+ pass
195
+
196
+ cheat_table.title = title
197
+ cheat_table.target_process = target_process
198
+
199
+ # Parse cheat entries
200
+ entries = []
201
+
202
+ # Look for CheatEntries container or direct CheatEntry elements
203
+ cheat_entries_elem = root.find('CheatEntries')
204
+ if cheat_entries_elem is not None:
205
+ # Entries are in a CheatEntries container
206
+ entry_elements = cheat_entries_elem.findall('CheatEntry')
207
+ else:
208
+ # Look for direct CheatEntry elements
209
+ entry_elements = root.findall('CheatEntry')
210
+
211
+ logger.info(f"Found {len(entry_elements)} cheat entries to parse")
212
+
213
+ for entry_elem in entry_elements:
214
+ entry = self._parse_xml_entry(entry_elem)
215
+ if entry:
216
+ entries.append(entry)
217
+
218
+ # Also parse child entries if they exist
219
+ child_entries = self._parse_xml_child_entries(entry_elem)
220
+ if child_entries:
221
+ entries.extend(child_entries)
222
+
223
+ cheat_table.entries = entries
224
+
225
+ # Parse structures
226
+ structures = []
227
+ structures_elem = root.find('Structures')
228
+ if structures_elem is not None:
229
+ logger.info("Found Structures section")
230
+ structures = self._parse_structures(structures_elem)
231
+
232
+ cheat_table.structures = structures
233
+
234
+ # Parse Lua script
235
+ lua_script = None
236
+ lua_elem = root.find('LuaScript')
237
+ if lua_elem is not None and lua_elem.text:
238
+ lua_script = lua_elem.text.strip()
239
+ logger.info(f"Found Lua script: {len(lua_script)} characters")
240
+
241
+ cheat_table.lua_script = lua_script
242
+
243
+ # Parse disassembler comments
244
+ disassembler_comments = []
245
+ comments_elem = root.find('DisassemblerComments')
246
+ if comments_elem is not None:
247
+ logger.info("Found DisassemblerComments section")
248
+ disassembler_comments = self._parse_disassembler_comments(comments_elem)
249
+
250
+ cheat_table.disassembler_comments = disassembler_comments
251
+
252
+ logger.info(f"Successfully parsed cheat table: {len(entries)} entries, {len(structures)} structures, "
253
+ f"lua_script: {'yes' if lua_script else 'no'}, {len(disassembler_comments)} comments")
254
+
255
+ return cheat_table
256
+
257
+ except Exception as e:
258
+ logger.error(f"Error parsing XML format: {e}")
259
+ return None
260
+
261
+ def _parse_binary_format_to_cheattable(self, file_handle: BinaryIO) -> Optional['CheatTable']:
262
+ """Parse binary format .CT file and convert to CheatTable"""
263
+ # Use existing binary parsing logic and convert to CheatTable
264
+ address_list = self._parse_binary_format(file_handle)
265
+ if address_list:
266
+ cheat_table = CheatTable(
267
+ title=address_list.title,
268
+ target_process=address_list.target_process,
269
+ entries=address_list.entries
270
+ )
271
+ if hasattr(address_list, 'structures'):
272
+ cheat_table.structures = address_list.structures
273
+ if hasattr(address_list, 'lua_script'):
274
+ cheat_table.lua_script = address_list.lua_script
275
+ if hasattr(address_list, 'disassembler_comments'):
276
+ cheat_table.disassembler_comments = address_list.disassembler_comments
277
+ return cheat_table
278
+ return None
279
+ """Parse a .CT file and return the address list
280
+
281
+ Args:
282
+ file_path: Path to the .CT file
283
+
284
+ Returns:
285
+ AddressList object or None if parsing failed
286
+ """
287
+ try:
288
+ if not Path(file_path).exists():
289
+ logger.error(f"Cheat table file not found: {file_path}")
290
+ return None
291
+
292
+ # Try to determine if it's binary or XML format
293
+ with open(file_path, 'rb') as f:
294
+ header = f.read(1024) # Read more data for better detection
295
+ f.seek(0)
296
+
297
+ # Check for XML indicators
298
+ if (b'<?xml' in header or
299
+ b'<CheatTable' in header or
300
+ b'<CheatEntries' in header):
301
+ # XML format (newer CE versions)
302
+ return self._parse_xml_format(f)
303
+ else:
304
+ # Binary format (older CE versions)
305
+ return self._parse_binary_format(f)
306
+
307
+ except Exception as e:
308
+ logger.error(f"Error parsing cheat table {file_path}: {e}")
309
+ return None
310
+
311
+
312
+ def _parse_xml_child_entries(self, parent_elem: ET.Element) -> List[CheatEntry]:
313
+ """Parse child cheat entries from XML elements"""
314
+ entries = []
315
+
316
+ child_entries_elem = parent_elem.find('CheatEntries')
317
+ if child_entries_elem is not None:
318
+ for entry_elem in child_entries_elem.findall('CheatEntry'):
319
+ entry = self._parse_xml_entry(entry_elem)
320
+ if entry:
321
+ entries.append(entry)
322
+
323
+ # Recursively parse nested child entries
324
+ nested_entries = self._parse_xml_child_entries(entry_elem)
325
+ if nested_entries:
326
+ entries.extend(nested_entries)
327
+
328
+ return entries
329
+
330
+ def _parse_structures(self, structures_elem: ET.Element) -> List[Structure]:
331
+ """Parse structures from XML elements"""
332
+ structures = []
333
+
334
+ for struct_elem in structures_elem.findall('Structure'):
335
+ try:
336
+ name = struct_elem.get('Name', 'Unknown')
337
+ auto_fill = struct_elem.get('AutoFill', '0') == '1'
338
+ auto_create = struct_elem.get('AutoCreate', '1') == '1'
339
+ default_hex = struct_elem.get('DefaultHex', '0') == '1'
340
+ auto_destroy = struct_elem.get('AutoDestroy', '0') == '1'
341
+
342
+ # Parse elements
343
+ elements = []
344
+ elements_elem = struct_elem.find('Elements')
345
+ if elements_elem is not None:
346
+ for element_elem in elements_elem.findall('Element'):
347
+ try:
348
+ offset_str = element_elem.get('Offset', '0')
349
+ # Handle hexadecimal offsets (e.g., '0x0', '0x10', etc.)
350
+ if offset_str.startswith('0x'):
351
+ offset = int(offset_str, 16)
352
+ else:
353
+ offset = int(offset_str)
354
+ vartype = element_elem.get('Vartype', '4 Bytes')
355
+ bytesize = int(element_elem.get('Bytesize', '4'))
356
+ description = element_elem.get('Description', 'Unknown')
357
+ display_method = element_elem.get('DisplayMethod', 'unsigned integer')
358
+ child_struct = element_elem.get('ChildStruct')
359
+
360
+ element = StructureElement(
361
+ offset=offset,
362
+ vartype=vartype,
363
+ bytesize=bytesize,
364
+ description=description,
365
+ display_method=display_method,
366
+ child_struct=child_struct
367
+ )
368
+ elements.append(element)
369
+
370
+ except (ValueError, TypeError) as e:
371
+ logger.warning(f"Error parsing structure element: {e}")
372
+ continue
373
+
374
+ structure = Structure(
375
+ name=name,
376
+ auto_fill=auto_fill,
377
+ auto_create=auto_create,
378
+ default_hex=default_hex,
379
+ auto_destroy=auto_destroy,
380
+ elements=elements
381
+ )
382
+ structures.append(structure)
383
+
384
+ except Exception as e:
385
+ logger.warning(f"Error parsing structure: {e}")
386
+ continue
387
+
388
+ return structures
389
+
390
+ def _parse_disassembler_comments(self, disassembler_elem: ET.Element) -> List[DisassemblerComment]:
391
+ """Parse disassembler comments from XML elements"""
392
+ comments = []
393
+
394
+ for comment_elem in disassembler_elem.findall('DisassemblerComment'):
395
+ try:
396
+ address_elem = comment_elem.find('Address')
397
+ comment_text_elem = comment_elem.find('Comment')
398
+
399
+ if address_elem is not None and comment_text_elem is not None:
400
+ address = address_elem.text.strip() if address_elem.text else ""
401
+ comment_text = comment_text_elem.text.strip() if comment_text_elem.text else ""
402
+
403
+ # Clean up address format (remove quotes)
404
+ address = address.strip('"\'')
405
+
406
+ comment = DisassemblerComment(
407
+ address=address,
408
+ comment=comment_text
409
+ )
410
+ comments.append(comment)
411
+
412
+ except Exception as e:
413
+ logger.warning(f"Error parsing disassembler comment: {e}")
414
+ continue
415
+
416
+ return comments
417
+
418
+ def _parse_xml_entry(self, entry_elem: ET.Element) -> Optional[CheatEntry]:
419
+ """Parse a single cheat entry from XML"""
420
+ try:
421
+ # Get basic attributes
422
+ entry_id = self._get_xml_text(entry_elem, 'ID', '')
423
+ description = self._get_xml_text(entry_elem, 'Description', 'Unknown')
424
+
425
+ # Remove quotes from description if present
426
+ description = description.strip('"\'')
427
+
428
+ # Check if it's a group header
429
+ group_header = entry_elem.get('GroupHeader', '0') == '1'
430
+
431
+ # Parse address information
432
+ address_text = self._get_xml_text(entry_elem, 'Address')
433
+ address = None
434
+ address_string = None
435
+
436
+ if address_text:
437
+ # Handle module-relative addresses like "D2GAME.dll+1107B8"
438
+ if '+' in address_text:
439
+ # Store the original address text for module+offset addresses
440
+ address_string = address_text
441
+ else:
442
+ # Try to parse as direct address
443
+ address = self._parse_address(address_text)
444
+
445
+ # Parse offsets
446
+ offsets = []
447
+ offsets_elem = entry_elem.find('Offsets')
448
+ if offsets_elem is not None:
449
+ for offset_elem in offsets_elem.findall('Offset'):
450
+ if offset_elem.text and offset_elem.text.strip():
451
+ try:
452
+ offset_text = offset_elem.text.strip()
453
+ if offset_text.startswith('0x'):
454
+ offset_val = int(offset_text, 16)
455
+ else:
456
+ offset_val = int(offset_text, 16) # Assume hex without 0x prefix
457
+ offsets.append(offset_val)
458
+ except ValueError:
459
+ logger.warning(f"Failed to parse offset: {offset_elem.text}")
460
+ continue
461
+
462
+ # Parse variable type
463
+ var_type = self._get_xml_text(entry_elem, 'VariableType', '4 Bytes')
464
+
465
+ # Parse ShowAsSigned
466
+ show_signed = self._get_xml_text(entry_elem, 'ShowAsSigned', '0') == '1'
467
+
468
+ # Parse ShowAsHex
469
+ show_hex = self._get_xml_text(entry_elem, 'ShowAsHex', '0') == '1'
470
+
471
+ # Parse value
472
+ value = None
473
+ value_elem = entry_elem.find('LastState')
474
+ if value_elem is not None and value_elem.get('Value'):
475
+ value = value_elem.get('Value')
476
+
477
+ # Parse enabled state
478
+ enabled = entry_elem.get('Enabled', '0') == '1'
479
+
480
+ # Parse hotkey
481
+ hotkey = None
482
+ hotkey_elem = entry_elem.find('Hotkeys/Hotkey')
483
+ if hotkey_elem is not None:
484
+ hotkey = hotkey_elem.get('Keys', '')
485
+
486
+ # Parse script
487
+ script = None
488
+ script_elem = entry_elem.find('LuaScript')
489
+ if script_elem is not None and script_elem.text:
490
+ script = script_elem.text.strip()
491
+
492
+ # Create entry with additional metadata
493
+ entry = CheatEntry(
494
+ id=entry_id,
495
+ description=description,
496
+ address=address,
497
+ address_string=address_string,
498
+ offsets=offsets,
499
+ variable_type=var_type,
500
+ value=value,
501
+ enabled=enabled,
502
+ hotkey=hotkey,
503
+ script=script,
504
+ group_header=group_header
505
+ )
506
+
507
+ # Store additional metadata as attributes
508
+ entry.show_signed = show_signed
509
+ entry.show_hex = show_hex
510
+ entry.original_address = address_text
511
+
512
+ return entry
513
+
514
+ except Exception as e:
515
+ logger.warning(f"Error parsing XML entry: {e}")
516
+ return None
517
+
518
+ def _get_xml_text(self, parent: ET.Element, tag: str, default: str = '') -> str:
519
+ """Get text content from XML element"""
520
+ elem = parent.find(tag)
521
+ if elem is not None and elem.text:
522
+ return elem.text.strip()
523
+ return default
524
+
525
+ def _parse_binary_format(self, file_handle: BinaryIO) -> Optional[AddressList]:
526
+ """Parse binary format .CT file (older Cheat Engine versions)"""
527
+ try:
528
+ # This is a simplified parser for older binary format
529
+ # The exact format varies by CE version and can be complex
530
+ logger.warning("Binary .CT format parsing has limited support")
531
+
532
+ # Try to read header
533
+ file_handle.seek(0)
534
+ data = file_handle.read()
535
+
536
+ # Look for text patterns that might indicate addresses/descriptions
537
+ entries = []
538
+
539
+ # Simple heuristic: look for potential address patterns
540
+ import re
541
+
542
+ # Look for hex addresses in the binary data
543
+ address_pattern = rb'[0-9A-Fa-f]{8}'
544
+ matches = re.finditer(address_pattern, data)
545
+
546
+ for i, match in enumerate(matches):
547
+ if i >= 50: # Limit results
548
+ break
549
+
550
+ try:
551
+ addr_str = match.group().decode('ascii')
552
+ address = int(addr_str, 16)
553
+
554
+ entry = CheatEntry(
555
+ id=f"binary_entry_{i}",
556
+ description=f"Address from binary table",
557
+ address=address,
558
+ variable_type="4 Bytes"
559
+ )
560
+ entries.append(entry)
561
+
562
+ except (ValueError, UnicodeDecodeError):
563
+ continue
564
+
565
+ return AddressList(
566
+ entries=entries,
567
+ title="Binary Cheat Table",
568
+ target_process=""
569
+ )
570
+
571
+ except Exception as e:
572
+ logger.error(f"Error parsing binary format: {e}")
573
+ return None
574
+
575
+ def _parse_address(self, address_str: str) -> Optional[int]:
576
+ """Parse an address string to integer"""
577
+ try:
578
+ if not address_str:
579
+ return None
580
+
581
+ address_str = address_str.strip()
582
+
583
+ # Handle different address formats
584
+ if address_str.startswith('0x'):
585
+ return int(address_str, 16)
586
+ elif address_str.startswith('$'):
587
+ return int(address_str[1:], 16)
588
+ elif all(c in '0123456789ABCDEFabcdef' for c in address_str):
589
+ return int(address_str, 16)
590
+ else:
591
+ # Might be a symbolic address or expression
592
+ return None
593
+
594
+ except ValueError:
595
+ return None
596
+
597
+ def export_to_dict(self, address_list: AddressList) -> Dict[str, Any]:
598
+ """Export address list to dictionary format"""
599
+ return {
600
+ 'title': address_list.title,
601
+ 'target_process': address_list.target_process,
602
+ 'entries': [
603
+ {
604
+ 'id': entry.id,
605
+ 'description': entry.description,
606
+ 'address': entry.address_string if entry.address_string else (f"0x{entry.address:08X}" if entry.address else None),
607
+ 'offsets': [f"0x{offset:X}" for offset in entry.offsets],
608
+ 'variable_type': entry.variable_type,
609
+ 'value': entry.value,
610
+ 'enabled': entry.enabled,
611
+ 'hotkey': entry.hotkey,
612
+ 'has_script': entry.script is not None,
613
+ 'group_header': entry.group_header
614
+ }
615
+ for entry in address_list.entries
616
+ ]
617
+ }
618
+
619
+ def create_mcp_tools_from_table(self, address_list: AddressList) -> List[Dict[str, Any]]:
620
+ """Create MCP tool definitions from cheat table entries
621
+
622
+ Args:
623
+ address_list: Parsed cheat table
624
+
625
+ Returns:
626
+ List of MCP tool definitions
627
+ """
628
+ tools = []
629
+
630
+ for entry in address_list.entries:
631
+ if entry.group_header or (not entry.address and not entry.address_string):
632
+ continue
633
+
634
+ # Create a read tool for this entry
635
+ tool_name = f"read_{entry.id}" if entry.id else f"read_entry_{len(tools)}"
636
+ tool_name = self._sanitize_tool_name(tool_name)
637
+
638
+ tool_def = {
639
+ "name": tool_name,
640
+ "description": f"Read {entry.description} ({entry.variable_type})",
641
+ "inputSchema": {
642
+ "type": "object",
643
+ "properties": {},
644
+ "required": []
645
+ },
646
+ "metadata": {
647
+ "cheat_entry": {
648
+ "address": entry.address_string if entry.address_string else entry.address,
649
+ "offsets": entry.offsets,
650
+ "type": entry.variable_type,
651
+ "description": entry.description
652
+ }
653
+ }
654
+ }
655
+
656
+ tools.append(tool_def)
657
+
658
+ return tools
659
+
660
+ def _sanitize_tool_name(self, name: str) -> str:
661
+ """Sanitize a tool name for MCP compatibility"""
662
+ import re
663
+ # Replace invalid characters with underscores
664
+ sanitized = re.sub(r'[^a-zA-Z0-9_]', '_', name)
665
+ # Ensure it starts with a letter or underscore
666
+ if sanitized and sanitized[0].isdigit():
667
+ sanitized = 'entry_' + sanitized
668
+ return sanitized or 'unnamed_entry'
669
+
670
+ def get_summary(self, address_list: AddressList) -> Dict[str, Any]:
671
+ """Get a summary of the cheat table"""
672
+ entry_types = {}
673
+ enabled_count = 0
674
+ script_count = 0
675
+ group_count = 0
676
+
677
+ for entry in address_list.entries:
678
+ # Count by type
679
+ var_type = entry.variable_type
680
+ entry_types[var_type] = entry_types.get(var_type, 0) + 1
681
+
682
+ # Count enabled entries
683
+ if entry.enabled:
684
+ enabled_count += 1
685
+
686
+ # Count scripts
687
+ if entry.script:
688
+ script_count += 1
689
+
690
+ # Count groups
691
+ if entry.group_header:
692
+ group_count += 1
693
+
694
+ return {
695
+ 'title': address_list.title,
696
+ 'target_process': address_list.target_process,
697
+ 'total_entries': len(address_list.entries),
698
+ 'enabled_entries': enabled_count,
699
+ 'group_headers': group_count,
700
+ 'script_entries': script_count,
701
+ 'entry_types': entry_types,
702
+ 'has_offsets': sum(1 for e in address_list.entries if e.offsets),
703
+ 'has_hotkeys': sum(1 for e in address_list.entries if e.hotkey)
704
+ }
705
+
706
+ def create_backup(self, file_path: str) -> str:
707
+ """
708
+ Create a backup of the cheat table file
709
+
710
+ Args:
711
+ file_path: Path to the original .CT file
712
+
713
+ Returns:
714
+ Path to the backup file
715
+ """
716
+ try:
717
+ original_path = Path(file_path)
718
+ if not original_path.exists():
719
+ raise FileNotFoundError(f"Original file not found: {file_path}")
720
+
721
+ # Create backup filename with timestamp
722
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
723
+ backup_name = f"{original_path.stem}_backup_{timestamp}{original_path.suffix}"
724
+ backup_path = original_path.parent / backup_name
725
+
726
+ # Copy the file
727
+ shutil.copy2(file_path, backup_path)
728
+ logger.info(f"Backup created: {backup_path}")
729
+
730
+ return str(backup_path)
731
+
732
+ except Exception as e:
733
+ logger.error(f"Error creating backup for {file_path}: {e}")
734
+ raise
735
+
736
+ def add_address_to_table(self, file_path: str, new_entry: CheatEntry, create_backup: bool = True) -> bool:
737
+ """
738
+ Add a new address entry to an existing cheat table
739
+
740
+ Args:
741
+ file_path: Path to the .CT file
742
+ new_entry: CheatEntry object to add
743
+ create_backup: Whether to create a backup before modifying
744
+
745
+ Returns:
746
+ True if successful, False otherwise
747
+ """
748
+ try:
749
+ if create_backup:
750
+ self.create_backup(file_path)
751
+
752
+ # Load existing table
753
+ address_list = self.parse_file(file_path)
754
+ if not address_list:
755
+ logger.error(f"Could not load existing table: {file_path}")
756
+ return False
757
+
758
+ # Add new entry
759
+ address_list.entries.append(new_entry)
760
+
761
+ # Write back to file
762
+ return self.write_table_to_file(file_path, address_list)
763
+
764
+ except Exception as e:
765
+ logger.error(f"Error adding address to table {file_path}: {e}")
766
+ return False
767
+
768
+ def write_table_to_file(self, file_path: str, address_list: 'AddressList') -> bool:
769
+ """
770
+ Write an AddressList to a .CT file in XML format
771
+
772
+ Args:
773
+ file_path: Path to write the .CT file
774
+ address_list: AddressList object to write
775
+
776
+ Returns:
777
+ True if successful, False otherwise
778
+ """
779
+ try:
780
+ # Create XML structure
781
+ root = ET.Element("CheatTable")
782
+
783
+ # Add CheatEntries section
784
+ cheat_entries = ET.SubElement(root, "CheatEntries")
785
+
786
+ for entry in address_list.entries:
787
+ self._write_cheat_entry_xml(cheat_entries, entry)
788
+
789
+ # Add structures if available
790
+ if hasattr(address_list, 'structures') and address_list.structures:
791
+ structures_elem = ET.SubElement(root, "Structures")
792
+ for structure in address_list.structures:
793
+ self._write_structure_xml(structures_elem, structure)
794
+
795
+ # Add Lua script if available
796
+ if hasattr(address_list, 'lua_script') and address_list.lua_script:
797
+ lua_elem = ET.SubElement(root, "LuaScript")
798
+ lua_elem.text = address_list.lua_script
799
+
800
+ # Add disassembler comments if available
801
+ if hasattr(address_list, 'disassembler_comments') and address_list.disassembler_comments:
802
+ comments_elem = ET.SubElement(root, "DisassemblerComments")
803
+ for comment in address_list.disassembler_comments:
804
+ self._write_disassembler_comment_xml(comments_elem, comment)
805
+
806
+ # Format XML with proper indentation
807
+ self._indent_xml(root)
808
+
809
+ # Write to file
810
+ tree = ET.ElementTree(root)
811
+ tree.write(file_path, encoding='utf-8', xml_declaration=True)
812
+
813
+ logger.info(f"Successfully wrote cheat table to: {file_path}")
814
+ return True
815
+
816
+ except Exception as e:
817
+ logger.error(f"Error writing table to file {file_path}: {e}")
818
+ return False
819
+
820
+ def _write_cheat_entry_xml(self, parent: ET.Element, entry: CheatEntry) -> None:
821
+ """Write a single CheatEntry to XML"""
822
+ cheat_entry = ET.SubElement(parent, "CheatEntry")
823
+
824
+ # Basic properties
825
+ ET.SubElement(cheat_entry, "ID").text = str(entry.id)
826
+ ET.SubElement(cheat_entry, "Description").text = entry.description or ""
827
+
828
+ # Handle both numeric addresses and module+offset addresses
829
+ if entry.address_string:
830
+ # Module+offset address (e.g., "D2GAME.dll+1107B8")
831
+ ET.SubElement(cheat_entry, "Address").text = entry.address_string
832
+ elif entry.address is not None:
833
+ # Numeric address
834
+ ET.SubElement(cheat_entry, "Address").text = hex(entry.address)
835
+
836
+ ET.SubElement(cheat_entry, "VariableType").text = entry.variable_type
837
+
838
+ if entry.offsets:
839
+ offsets_elem = ET.SubElement(cheat_entry, "Offsets")
840
+ for offset in entry.offsets:
841
+ ET.SubElement(offsets_elem, "Offset").text = hex(offset)
842
+
843
+ if entry.value is not None:
844
+ ET.SubElement(cheat_entry, "Value").text = str(entry.value)
845
+
846
+ if entry.enabled:
847
+ ET.SubElement(cheat_entry, "Enabled").text = "1"
848
+
849
+ if entry.hotkey:
850
+ ET.SubElement(cheat_entry, "Hotkey").text = entry.hotkey
851
+
852
+ if entry.script:
853
+ ET.SubElement(cheat_entry, "Script").text = entry.script
854
+
855
+ if entry.group_header:
856
+ ET.SubElement(cheat_entry, "GroupHeader").text = "1"
857
+
858
+ def _write_structure_xml(self, parent: ET.Element, structure: Structure) -> None:
859
+ """Write a Structure to XML"""
860
+ struct_elem = ET.SubElement(parent, "Structure")
861
+ struct_elem.set("Name", structure.name)
862
+ struct_elem.set("AutoFill", "1" if structure.auto_fill else "0")
863
+ struct_elem.set("AutoCreate", "1" if structure.auto_create else "0")
864
+ struct_elem.set("DefaultHex", "1" if structure.default_hex else "0")
865
+ struct_elem.set("AutoDestroy", "1" if structure.auto_destroy else "0")
866
+
867
+ elements_elem = ET.SubElement(struct_elem, "Elements")
868
+ for element in structure.elements:
869
+ elem = ET.SubElement(elements_elem, "Element")
870
+ elem.set("Offset", hex(element.offset))
871
+ elem.set("Vartype", element.vartype)
872
+ elem.set("Bytesize", str(element.bytesize))
873
+ elem.set("Description", element.description)
874
+ elem.set("DisplayMethod", element.display_method)
875
+ if element.child_struct:
876
+ elem.set("ChildStruct", element.child_struct)
877
+
878
+ def _write_disassembler_comment_xml(self, parent: ET.Element, comment: 'DisassemblerComment') -> None:
879
+ """Write a DisassemblerComment to XML"""
880
+ comment_elem = ET.SubElement(parent, "Comment")
881
+ comment_elem.set("Address", comment.address)
882
+ comment_elem.text = comment.comment
883
+
884
+ def _indent_xml(self, elem: ET.Element, level: int = 0) -> None:
885
+ """Add proper indentation to XML for readability"""
886
+ i = "\n" + level * " "
887
+ if len(elem):
888
+ if not elem.text or not elem.text.strip():
889
+ elem.text = i + " "
890
+ if not elem.tail or not elem.tail.strip():
891
+ elem.tail = i
892
+ for elem in elem:
893
+ self._indent_xml(elem, level + 1)
894
+ if not elem.tail or not elem.tail.strip():
895
+ elem.tail = i
896
+ else:
897
+ if level and (not elem.tail or not elem.tail.strip()):
898
+ elem.tail = i
899
+
900
+ def modify_address_in_table(self, file_path: str, entry_id: str, updated_entry: CheatEntry, create_backup: bool = True) -> bool:
901
+ """
902
+ Modify an existing address entry in a cheat table
903
+
904
+ Args:
905
+ file_path: Path to the .CT file
906
+ entry_id: ID of the entry to modify
907
+ updated_entry: Updated CheatEntry object
908
+ create_backup: Whether to create a backup before modifying
909
+
910
+ Returns:
911
+ True if successful, False otherwise
912
+ """
913
+ try:
914
+ if create_backup:
915
+ self.create_backup(file_path)
916
+
917
+ # Load existing table
918
+ address_list = self.parse_file(file_path)
919
+ if not address_list:
920
+ logger.error(f"Could not load existing table: {file_path}")
921
+ return False
922
+
923
+ # Find and update entry
924
+ found = False
925
+ for i, entry in enumerate(address_list.entries):
926
+ if entry.id == entry_id:
927
+ address_list.entries[i] = updated_entry
928
+ found = True
929
+ break
930
+
931
+ if not found:
932
+ logger.error(f"Entry with ID {entry_id} not found")
933
+ return False
934
+
935
+ # Write back to file
936
+ return self.write_table_to_file(file_path, address_list)
937
+
938
+ except Exception as e:
939
+ logger.error(f"Error modifying address in table {file_path}: {e}")
940
+ return False
941
+
942
+ def remove_address_from_table(self, file_path: str, entry_id: str, create_backup: bool = True) -> bool:
943
+ """
944
+ Remove an address entry from a cheat table
945
+
946
+ Args:
947
+ file_path: Path to the .CT file
948
+ entry_id: ID of the entry to remove
949
+ create_backup: Whether to create a backup before modifying
950
+
951
+ Returns:
952
+ True if successful, False otherwise
953
+ """
954
+ try:
955
+ if create_backup:
956
+ self.create_backup(file_path)
957
+
958
+ # Load existing table
959
+ address_list = self.parse_file(file_path)
960
+ if not address_list:
961
+ logger.error(f"Could not load existing table: {file_path}")
962
+ return False
963
+
964
+ # Find and remove entry
965
+ original_count = len(address_list.entries)
966
+ address_list.entries = [entry for entry in address_list.entries if entry.id != entry_id]
967
+
968
+ if len(address_list.entries) == original_count:
969
+ logger.error(f"Entry with ID {entry_id} not found")
970
+ return False
971
+
972
+ # Write back to file
973
+ return self.write_table_to_file(file_path, address_list)
974
+
975
+ except Exception as e:
976
+ logger.error(f"Error removing address from table {file_path}: {e}")
977
+ return False
978
+
979
+ def create_new_table(self, file_path: str, title: str = "New Cheat Table", target_process: str = "") -> bool:
980
+ """
981
+ Create a new empty cheat table file
982
+
983
+ Args:
984
+ file_path: Path where to create the .CT file
985
+ title: Title for the cheat table
986
+ target_process: Target process name
987
+
988
+ Returns:
989
+ True if successful, False otherwise
990
+ """
991
+ try:
992
+ # Create empty AddressList
993
+ address_list = AddressList(
994
+ title=title,
995
+ target_process=target_process,
996
+ entries=[]
997
+ )
998
+
999
+ # Write to file
1000
+ return self.write_table_to_file(file_path, address_list)
1001
+
1002
+ except Exception as e:
1003
+ logger.error(f"Error creating new table {file_path}: {e}")
1004
+ return False
1005
+
1006
+ def write_cheat_table_preserving_structure(self, file_path: str, cheat_table: 'CheatTable') -> bool:
1007
+ """
1008
+ Write a CheatTable to file while preserving original XML structure
1009
+ Only modifies the parts that have been changed
1010
+
1011
+ Args:
1012
+ file_path: Path to write the .CT file
1013
+ cheat_table: CheatTable object to write
1014
+
1015
+ Returns:
1016
+ True if successful, False otherwise
1017
+ """
1018
+ try:
1019
+ if cheat_table._original_xml_root is not None:
1020
+ # Use original XML structure and modify only changed parts
1021
+ return self._write_preserving_original_xml(file_path, cheat_table)
1022
+ else:
1023
+ # Create new XML structure
1024
+ return self._write_new_xml_structure(file_path, cheat_table)
1025
+
1026
+ except Exception as e:
1027
+ logger.error(f"Error writing cheat table to file {file_path}: {e}")
1028
+ return False
1029
+
1030
+ def _write_preserving_original_xml(self, file_path: str, cheat_table: 'CheatTable') -> bool:
1031
+ """Write using original XML structure, modifying only changed parts"""
1032
+ try:
1033
+ # Create a copy of the original XML root
1034
+ import copy
1035
+ root = copy.deepcopy(cheat_table._original_xml_root)
1036
+
1037
+ # Update CheatEntries section
1038
+ cheat_entries_elem = root.find('CheatEntries')
1039
+ if cheat_entries_elem is not None:
1040
+ # Clear existing entries and add updated ones
1041
+ cheat_entries_elem.clear()
1042
+ for entry in cheat_table.entries:
1043
+ self._write_cheat_entry_xml(cheat_entries_elem, entry)
1044
+ else:
1045
+ # Create CheatEntries section if it doesn't exist
1046
+ cheat_entries_elem = ET.SubElement(root, 'CheatEntries')
1047
+ for entry in cheat_table.entries:
1048
+ self._write_cheat_entry_xml(cheat_entries_elem, entry)
1049
+
1050
+ # Update Structures section if modified
1051
+ structures_elem = root.find('Structures')
1052
+ if cheat_table.structures:
1053
+ if structures_elem is None:
1054
+ structures_elem = ET.SubElement(root, 'Structures')
1055
+ else:
1056
+ structures_elem.clear()
1057
+
1058
+ for structure in cheat_table.structures:
1059
+ self._write_structure_xml(structures_elem, structure)
1060
+
1061
+ # Update Lua script if modified
1062
+ lua_elem = root.find('LuaScript')
1063
+ if cheat_table.lua_script:
1064
+ if lua_elem is None:
1065
+ lua_elem = ET.SubElement(root, 'LuaScript')
1066
+ lua_elem.text = cheat_table.lua_script
1067
+
1068
+ # Update disassembler comments if modified
1069
+ comments_elem = root.find('DisassemblerComments')
1070
+ if cheat_table.disassembler_comments:
1071
+ if comments_elem is None:
1072
+ comments_elem = ET.SubElement(root, 'DisassemblerComments')
1073
+ else:
1074
+ comments_elem.clear()
1075
+
1076
+ for comment in cheat_table.disassembler_comments:
1077
+ self._write_disassembler_comment_xml(comments_elem, comment)
1078
+
1079
+ # Preserve XML formatting
1080
+ self._preserve_xml_formatting(root)
1081
+
1082
+ # Write to file with original encoding and declaration
1083
+ tree = ET.ElementTree(root)
1084
+
1085
+ # Try to preserve original XML declaration
1086
+ if cheat_table._original_xml_content and cheat_table._original_xml_content.startswith('<?xml'):
1087
+ # Extract and preserve original XML declaration
1088
+ xml_decl_end = cheat_table._original_xml_content.find('?>') + 2
1089
+ xml_declaration = cheat_table._original_xml_content[:xml_decl_end]
1090
+
1091
+ # Write with preserved declaration
1092
+ with open(file_path, 'w', encoding='utf-8') as f:
1093
+ f.write(xml_declaration + '\n')
1094
+ tree.write(f, encoding='unicode', xml_declaration=False)
1095
+ else:
1096
+ tree.write(file_path, encoding='utf-8', xml_declaration=True)
1097
+
1098
+ logger.info(f"Successfully wrote cheat table preserving structure to: {file_path}")
1099
+ return True
1100
+
1101
+ except Exception as e:
1102
+ logger.error(f"Error writing with preserved structure: {e}")
1103
+ return False
1104
+
1105
+ def _write_new_xml_structure(self, file_path: str, cheat_table: 'CheatTable') -> bool:
1106
+ """Write using new XML structure"""
1107
+ try:
1108
+ # Create XML structure
1109
+ root = ET.Element("CheatTable")
1110
+
1111
+ # Add CheatEntries section
1112
+ cheat_entries = ET.SubElement(root, "CheatEntries")
1113
+
1114
+ for entry in cheat_table.entries:
1115
+ self._write_cheat_entry_xml(cheat_entries, entry)
1116
+
1117
+ # Add structures if available
1118
+ if cheat_table.structures:
1119
+ structures_elem = ET.SubElement(root, "Structures")
1120
+ for structure in cheat_table.structures:
1121
+ self._write_structure_xml(structures_elem, structure)
1122
+
1123
+ # Add Lua script if available
1124
+ if cheat_table.lua_script:
1125
+ lua_elem = ET.SubElement(root, "LuaScript")
1126
+ lua_elem.text = cheat_table.lua_script
1127
+
1128
+ # Add disassembler comments if available
1129
+ if cheat_table.disassembler_comments:
1130
+ comments_elem = ET.SubElement(root, "DisassemblerComments")
1131
+ for comment in cheat_table.disassembler_comments:
1132
+ self._write_disassembler_comment_xml(comments_elem, comment)
1133
+
1134
+ # Format XML with proper indentation
1135
+ self._indent_xml(root)
1136
+
1137
+ # Write to file
1138
+ tree = ET.ElementTree(root)
1139
+ tree.write(file_path, encoding='utf-8', xml_declaration=True)
1140
+
1141
+ logger.info(f"Successfully wrote new cheat table structure to: {file_path}")
1142
+ return True
1143
+
1144
+ except Exception as e:
1145
+ logger.error(f"Error writing new XML structure: {e}")
1146
+ return False
1147
+
1148
+ def _preserve_xml_formatting(self, elem: ET.Element, level: int = 0) -> None:
1149
+ """Preserve XML formatting similar to original"""
1150
+ i = "\n" + level * " "
1151
+ if len(elem):
1152
+ if not elem.text or not elem.text.strip():
1153
+ elem.text = i + " "
1154
+ if not elem.tail or not elem.tail.strip():
1155
+ elem.tail = i
1156
+ for child in elem:
1157
+ self._preserve_xml_formatting(child, level + 1)
1158
+ if not elem.tail or not elem.tail.strip():
1159
+ elem.tail = i
1160
+ else:
1161
+ if level and (not elem.tail or not elem.tail.strip()):
1162
+ elem.tail = i
1163
+
1164
+ def add_address_to_cheat_table(self, file_path: str, new_entry: CheatEntry, create_backup: bool = True) -> bool:
1165
+ """
1166
+ Add a new address entry to an existing cheat table with structure preservation
1167
+
1168
+ Args:
1169
+ file_path: Path to the .CT file
1170
+ new_entry: CheatEntry object to add
1171
+ create_backup: Whether to create a backup before modifying
1172
+
1173
+ Returns:
1174
+ True if successful, False otherwise
1175
+ """
1176
+ try:
1177
+ if create_backup:
1178
+ self.create_backup(file_path)
1179
+
1180
+ # Load existing table with full structure preservation
1181
+ cheat_table = self.parse_file(file_path)
1182
+ if not cheat_table:
1183
+ logger.error(f"Could not load existing table: {file_path}")
1184
+ return False
1185
+
1186
+ # Add new entry
1187
+ cheat_table.entries.append(new_entry)
1188
+
1189
+ # Write back to file preserving structure
1190
+ return self.write_cheat_table_preserving_structure(file_path, cheat_table)
1191
+
1192
+ except Exception as e:
1193
+ logger.error(f"Error adding address to table {file_path}: {e}")
1194
+ return False
1195
+
1196
+ def parse_file_to_addresslist(self, file_path: str) -> Optional['AddressList']:
1197
+ """
1198
+ Parse a .CT file and return an AddressList object (for backwards compatibility)
1199
+
1200
+ Args:
1201
+ file_path: Path to the .CT file
1202
+
1203
+ Returns:
1204
+ AddressList object or None if parsing failed
1205
+ """
1206
+ cheat_table = self.parse_file(file_path)
1207
+ if cheat_table:
1208
+ return self._convert_cheattable_to_addresslist(cheat_table)
1209
+ return None
1210
+
1211
+ def _convert_cheattable_to_addresslist(self, cheat_table: 'CheatTable') -> 'AddressList':
1212
+ """Convert CheatTable to AddressList for backwards compatibility"""
1213
+ address_list = AddressList(
1214
+ title=cheat_table.title,
1215
+ target_process=cheat_table.target_process,
1216
+ entries=cheat_table.entries
1217
+ )
1218
+ address_list.structures = cheat_table.structures
1219
+ address_list.lua_script = cheat_table.lua_script
1220
+ address_list.disassembler_comments = cheat_table.disassembler_comments
1221
+ return address_list