-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathIPSPatch.py
107 lines (80 loc) · 3.29 KB
/
IPSPatch.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""
EarthBound Patcher - An easy-to-use EarthBound ROM patcher.
Copyright (C) 2013 Lyrositor <[email protected]>
This file is part of EarthBound Patcher.
EarthBound Patcher is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
EarthBound Patcher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with EarthBound Patcher. If not, see <http://www.gnu.org/licenses/>.
"""
# IPSPatch
# Handles the import of IPS patches (legacy).
# Heavily based on the python-ips module.
from io import BytesIO
import os
import struct
class IPSPatch(BytesIO):
"""The legacy patch format class, used to import patches."""
def __init__(self, patchPath, new=False):
"""Loads an existing IPS patch."""
if not new:
# Initialize the data.
super().__init__(open(patchPath, "rb").read())
self.header = 0
# Check its validity and load the records.
self.valid = self.checkValidity()
self.records = self.loadRecords()
if self.valid and self.records:
print("IPSPatch.__init__(): Valid IPS patch.")
else:
print("IPSPatch.__init__(): Invalid IPS patch.")
def checkValidity(self):
"""Checks whether or not this patch is valid."""
if self.read(5) != b"PATCH":
return False
return True
def loadRecords(self):
"""Loads the IPS records from the patch."""
# Position the cursor at the start of the first record.
self.seek(5)
# Start loading the records.
records = {}
while True:
# Read until an EOF is encountered.
i = self.read(3)
if i == b"EOF":
break
# If the end has been reached, the patch is corrupt.
if not i:
return None
self.seek(-3, os.SEEK_CUR)
# Get the record details.
offset = int.from_bytes(self.read(3), "big")
size = int.from_bytes(self.read(2), "big")
# If it's an RLE record, treat it as such.
if size == 0:
i = self.read(2)
sizeRLE = int.from_bytes(i, "big")
diff = self.read(1) * sizeRLE
# Otherwise, read it as a normal record.
else:
diff = self.read(size)
records[offset] = diff
return records
def applyToTarget(self, rom):
"""Applies the patch to the target ROM's data."""
# Expand the ROM if necessary.
last = sorted(self.records)[len(self.records) - 1]
newSize = last + len(self.records[last]) - self.header
if newSize > len(rom.getvalue()):
rom.modifySize(newSize)
# Apply the records.
for offset, diff in self.records.items():
rom.seek(offset - self.header)
rom.write(diff)