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,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
|