retrotool 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.
retrotool/script.py ADDED
@@ -0,0 +1,313 @@
1
+
2
+
3
+ class Table:
4
+ def __init__(self, table_file):
5
+ """
6
+ Load and interpret the table file, set up class variables
7
+ :param table_file: the path to the table file
8
+ """
9
+ enc, val_map, char_map, err_count, cnt = self._load_table(table_file)
10
+ self.__val_map = val_map
11
+ self.__chr_map = char_map
12
+ self.__errors = err_count
13
+ self.__parsed_lines = cnt
14
+ self.__file_name = table_file
15
+ self.__encoding = enc
16
+
17
+ def _load_table(self, table_file, enc=None):
18
+ """
19
+ Load a given table file. Supports encoding type overrides.
20
+ :param table_file: the path to the table file
21
+ :param enc: Optional Encoding type
22
+ :return: encoding, value_map, character_map, error_count, line_count
23
+ """
24
+ enc = enc if enc is not None else self.detect_encoding(table_file)
25
+ val_map = {}
26
+ char_map = {}
27
+ err_count = 0
28
+ cnt = 1
29
+ with open(table_file, encoding=enc) as to:
30
+ line = to.readline()
31
+ while line:
32
+ try:
33
+ # split the line using the '=' sign, ignore all else including
34
+ line_data = line.split('=')
35
+ if len(line_data) == 2:
36
+ # value first, character second
37
+ val = line_data[0]
38
+ ch = line_data[1]
39
+
40
+ # character will have new line codes removed unless the backslash-escape '\\' is used
41
+ ch = ch.replace('\n', '').replace('\r', '').replace('\\n', '\n')
42
+
43
+ # supports variable filling for table files
44
+ if self.exists(val, '**'):
45
+ # fill the table with equivalent values for a byte range
46
+ for d in range(0, 256):
47
+ prep_ch = ch.replace('**', self.hex(d))
48
+ prep_val = val.replace('**', self.hex(d))
49
+ if self.exists(val, '%%'):
50
+ for e in range(0, 256):
51
+ ch_val = prep_ch.replace('%%', self.hex(e))
52
+ val_val = prep_val.replace('%%', self.hex(e))
53
+
54
+ self._set_maps(val_val, ch_val, val_map, char_map)
55
+ else:
56
+ self._set_maps(prep_val, prep_ch, val_map, char_map)
57
+ else:
58
+ self._set_maps(val, ch, val_map, char_map)
59
+ except Exception as ex:
60
+ print(f"ERROR: {repr(ex)}")
61
+ err_count += 1
62
+ # read next line and increment line count
63
+ line = to.readline()
64
+ cnt += 1
65
+ return enc, val_map, char_map, err_count, cnt
66
+
67
+ @staticmethod
68
+ def _set_maps(in_val, in_ch, val_map, char_map):
69
+ """
70
+ Add the value and character to the map objects
71
+ :param in_val: value
72
+ :param in_ch: character
73
+ :param val_map: value map
74
+ :param char_map: character map
75
+ """
76
+ dec_val = int(in_val, 16)
77
+
78
+ if dec_val not in val_map.keys():
79
+ val_map[dec_val] = in_ch
80
+ if in_ch not in char_map.keys():
81
+ char_map[in_ch] = dec_val
82
+
83
+ @property
84
+ def encoding(self):
85
+ return self.__encoding
86
+
87
+ @staticmethod
88
+ def exists(str_val: str, search: str):
89
+ """
90
+ Search a given string for another string
91
+ :param str_val: searchable string
92
+ :param search: Value to search for
93
+ :return: True/False
94
+ """
95
+ try:
96
+ str_val.index(search)
97
+ return True
98
+ except ValueError:
99
+ return False
100
+
101
+ def get_value(self, word: str, infer_value=True):
102
+ """
103
+ Return value for a string
104
+ :param word: character/word string
105
+ :param infer_value: if the word comes in [00] format
106
+ :return: The value or None
107
+ """
108
+ if type(word) is not str:
109
+ raise ValueError("Value must be a string!")
110
+ if word in self.__chr_map.keys():
111
+ return self.__chr_map[word]
112
+ if infer_value and '[' in word and ']' in word:
113
+ try:
114
+ word = word.replace('[', '').replace(']', '')
115
+ return int(word, 16)
116
+ except:
117
+ print("Warning: Value could not be determined.")
118
+ return None
119
+
120
+ def get_chars(self, value: int, return_hex_repr=True):
121
+ if value in self.__val_map.keys():
122
+ return self.__val_map[value]
123
+ return f'[{self.hex(value)}]' if return_hex_repr else None
124
+
125
+ @staticmethod
126
+ def hex(value):
127
+ """
128
+ Return a hex representation
129
+ Only currently supports up to 64 bit encoded characters
130
+ :param value: the input value
131
+ :return: String representation of the given value
132
+ """
133
+ if value < 0x100:
134
+ pad = 2
135
+ elif value < 0x10000:
136
+ pad = 4
137
+ elif value < 0x1000000:
138
+ pad = 6
139
+ elif value < 0x100000000:
140
+ pad = 8
141
+ else:
142
+ raise ValueError("Error: Table Value is not supported!")
143
+ return f'{value:0{pad}X}'
144
+
145
+ @staticmethod
146
+ def byte_size(value: int):
147
+ """
148
+ Get the number of bytes representing the value
149
+ :param value: integer value
150
+ :return: number of bytes
151
+ """
152
+ from math import log
153
+ if value == 0:
154
+ return 1
155
+ return int(log(value, 256)) + 1
156
+
157
+ @staticmethod
158
+ def __get_byte_multiplier(value):
159
+ if value == 0:
160
+ return 1
161
+ final = '1'
162
+ for i in range(0, value):
163
+ final += '00'
164
+ return int(final, 16)
165
+
166
+ @staticmethod
167
+ def bytes_to_val(byte_list: list, reverse=False):
168
+ final_val = 0
169
+ if reverse:
170
+ byte_list.reverse()
171
+ for b in range(0, len(byte_list)):
172
+ final_val |= (byte_list[b] << (b * 8)) # self.__get_byte_multiplier(b)
173
+ return final_val
174
+
175
+ def interpret_binary(self, input_filename, max_bytes=3):
176
+ with open(input_filename, "rb") as data_file:
177
+ bin_data = list(data_file.read())
178
+ return self.interpret_binary_data(bin_data, max_bytes)
179
+
180
+ def interpret_binary_data(self, bin_data, max_bytes=3, trim_bytes=None):
181
+ final_string = ''
182
+ i = 0
183
+
184
+ # can trim certain expected bytes from the end of each output string
185
+ if trim_bytes is not None:
186
+ if type(trim_bytes) is int:
187
+ trim_bytes = [trim_bytes]
188
+ if trim_bytes is not None and len(trim_bytes) > 0:
189
+ exclude_count = 0
190
+ for i in range(len(bin_data), 0):
191
+ if bin_data[i] in trim_bytes:
192
+ exclude_count += 1
193
+ else:
194
+ break
195
+ if exclude_count > 0:
196
+ bin_data = bin_data[:len(bin_data)-exclude_count]
197
+
198
+ while i <= len(bin_data) + 1:
199
+ len_check = max_bytes
200
+ char = None
201
+ found_char = False
202
+ while len_check > 0:
203
+ end_check = i + len_check
204
+ val = self.bytes_to_val(bin_data[i: end_check], True)
205
+ char = self.get_chars(val, False)
206
+ if char:
207
+ found_char = True
208
+ i += (len_check - 1)
209
+ len_check = 0
210
+ else:
211
+ len_check -= 1
212
+
213
+ if not found_char:
214
+ char = self.get_chars(bin_data[i], True)
215
+ if char is None:
216
+ print(f"ERROR - Unable to resolve byte ({hex(bin_data[i])})???")
217
+ else:
218
+ final_string += char
219
+ i += 1
220
+ return final_string
221
+
222
+ def has_char(self, bin_data):
223
+ """
224
+ Check for a valid character using all given bytes
225
+ :param bin_data: the list of bytes
226
+ :return: if there is a valid character using these bytes
227
+ """
228
+ # get the value by OR'ing and shifting the bytes together
229
+ val = self.bytes_to_val(bin_data)
230
+
231
+ # check for valid value...
232
+ if len(bin_data) > 1 and val in bin_data:
233
+ # in the case of [00, 00] or [00, 90] or etc.
234
+ # the total value cannot equal a single byte value
235
+ return None
236
+
237
+ # using the value, check for a defined character
238
+ char = self.get_chars(val, False)
239
+
240
+ return char
241
+
242
+ def check_for_lone_byte(self, bin_data, index, value=0x0):
243
+ """
244
+ Used to check for the end of a text block
245
+ :param bin_data: a list of values
246
+ :param index: current list index
247
+ :param value: check for this value
248
+ :return: if the value is found,
249
+ check to see if it is part of a larger value
250
+ """
251
+ start1 = index - 3
252
+ start2 = index - 2
253
+ end0 = index + 1
254
+
255
+ if bin_data[index] == value:
256
+ char1 = self.has_char(bin_data[start1: end0])
257
+ char2 = self.has_char(bin_data[start2: end0])
258
+
259
+ if char1 is not None or char2 is not None:
260
+ return 0, char1 or char2
261
+
262
+ return -1, None
263
+
264
+ return 0, None
265
+
266
+ @staticmethod
267
+ def detect_encoding(file_path, lines=80):
268
+ """
269
+ Given a file, use the first X number of lines to detect the encoding
270
+ :param file_path: path to text file
271
+ :param lines: defaults to 80
272
+ :return: the assumed file encoding using chardet
273
+ """
274
+ import chardet
275
+ with open(file_path, 'rb') as f:
276
+ raw_data = b''.join([f.readline() for _ in range(lines)])
277
+ return chardet.detect(raw_data)['encoding']
278
+
279
+ def dump_script(self, filename: str, dict_data: list, deduplicate=True):
280
+ """
281
+ Dump the script using a table from a list of mapped data (id, addr, data)
282
+ :param filename: the output file path
283
+ :param dict_data: a list of mapped data (id (formatted), addr (must be int), data (list of binary data))
284
+ :param deduplicate: only supported if the addr key is given. will only display pointer map for data in the dump
285
+ """
286
+ line1 = True
287
+ nl = "\n"
288
+ with open(filename, 'w', encoding=self.encoding) as of:
289
+ dumped_addrs = []
290
+ for data in dict_data:
291
+ of.write(f"{'' if line1 else nl}<<{data.get('id')}>>{nl}")
292
+ addr = data.get('addr', None)
293
+ if deduplicate and addr is not None:
294
+ if addr not in dumped_addrs:
295
+ dumped_addrs.append(addr)
296
+ of.write(self.interpret_binary_data(data['data']))
297
+ else:
298
+ of.write(self.interpret_binary_data(data['data']))
299
+ line1 = False
300
+
301
+ @staticmethod
302
+ def export_csv(filename, dict_data: list):
303
+ import csv
304
+ csv_columns = dict_data[0].keys()
305
+ csv_file = f"./{filename}.csv"
306
+ try:
307
+ with open(csv_file, 'w', newline='') as csvfile:
308
+ writer = csv.DictWriter(csvfile, fieldnames=csv_columns)
309
+ writer.writeheader()
310
+ for data in dict_data:
311
+ writer.writerow(data)
312
+ except IOError:
313
+ print("I/O error")
retrotool/snes.py ADDED
@@ -0,0 +1,578 @@
1
+ from typing import Optional, Union
2
+ from functools import lru_cache
3
+ """
4
+ Version 2021.3
5
+ by DackR
6
+ """
7
+
8
+
9
+ class SFCPointer:
10
+ def __init__(self, low=None, high=None, bank=None):
11
+ """
12
+ Pointers can be defined, modified, and read in many ways.
13
+ low and high values can be used to fill part of, or the entire address, depending on what is desired
14
+ :param low: can be as small as 8 bit, as big as 24 bit (over 24 bit is ignored)
15
+ :param high: can be 8 to 16 bit (over 16 bit is ignored)
16
+ :param bank: only be 8 bit (extra data is lost)
17
+ """
18
+ self.__full_pointer = [0x0, 0x0, 0x0]
19
+ valid = self.validate_bytes(low, high, bank)
20
+ self.__set_ptr_pos(0, valid)
21
+ self.__set_ptr_pos(1, valid)
22
+ self.__set_ptr_pos(2, valid)
23
+
24
+ def __str__(self):
25
+ """
26
+ return the formatted address
27
+ """
28
+ return self.hex_fmt(self.full_address, 6) if self.full_address > 0xFFFF else self.hex_fmt(self.short_address, 4)
29
+
30
+ def __repr__(self):
31
+ return str(self)
32
+
33
+ @classmethod
34
+ def validate_bytes(cls, *args):
35
+ """
36
+ Values are passed in as positional arguments, low, high, and bank
37
+ low value can be the entire 24 bit address if no other args are passed in
38
+ high value can be the bank and high bytes if no bank byte is passed in
39
+ bank value can only be 8 bits and values higher are truncated
40
+ """
41
+ low = args[0] if len(args) > 0 else 0x0
42
+ high = args[1] if len(args) > 1 else 0x0
43
+ bank = args[2] if len(args) > 2 else 0x0
44
+ if low:
45
+ low = cls.integer_or_hex(low, 0xFFFFFF)
46
+ if low > 0xFFFF and not (high and bank):
47
+ bank = SFCAddress.bank_byte(low)
48
+ high = SFCAddress.high_byte(low)
49
+ low = SFCAddress.low_byte(low)
50
+ elif low > 0xFF and not high:
51
+ high = SFCAddress.high_byte(low)
52
+ low = SFCAddress.low_byte(low)
53
+ if high:
54
+ high = cls.integer_or_hex(high, 0xFFFF)
55
+ if high > 0xFF and not bank:
56
+ bank = SFCAddress.high_byte(high)
57
+ high = SFCAddress.low_byte(high)
58
+ if bank:
59
+ bank = cls.integer_or_hex(bank)
60
+ return low, high, bank
61
+
62
+ @staticmethod
63
+ def hex_fmt(value, pad=4, prefix='0x'):
64
+ """
65
+ Produce a formatted, hex value.
66
+ default is padded with a prefix
67
+ :param value: integer value to format as hex string
68
+ :param pad: padded up to 4 characters by default
69
+ :param prefix: prefix the hex string with any string -- '0x' by default
70
+ """
71
+ return f'{prefix}{value:0{pad}X}'
72
+
73
+ @property
74
+ def full_address(self):
75
+ return (self.__full_pointer[0]) + (self.__full_pointer[1] * 0x100) + (self.__full_pointer[2] * 0x10000)
76
+
77
+ @property
78
+ def full_hex(self):
79
+ return self.hex_fmt(self.full_address, 6)
80
+
81
+ @property
82
+ def short_address(self):
83
+ return (self.__full_pointer[0]) + (self.__full_pointer[1] * 0x100)
84
+
85
+ @property
86
+ def short_hex(self):
87
+ return self.hex_fmt(self.short_address)
88
+
89
+ @property
90
+ def short(self):
91
+ return self.__full_pointer[:2]
92
+
93
+ @short.setter
94
+ def short(self, value):
95
+ self.__full_pointer = [0x0, 0x0, 0x0]
96
+ self.__check_list_tuple(value)
97
+ self.__set_ptr_pos(0, value)
98
+ self.__set_ptr_pos(1, value)
99
+
100
+ @property
101
+ def full(self):
102
+ return self.__full_pointer
103
+
104
+ @full.setter
105
+ def full(self, value):
106
+ self.__full_pointer = [0x0, 0x0, 0x0]
107
+ self.__check_list_tuple(value)
108
+ self.__set_ptr_pos(0, value)
109
+ self.__set_ptr_pos(1, value)
110
+ self.__set_ptr_pos(2, value)
111
+
112
+ @property
113
+ def low(self):
114
+ return self.__full_pointer[0]
115
+
116
+ @low.setter
117
+ def low(self, value):
118
+ self.__full_pointer[0] = self.integer_or_hex(value)
119
+
120
+ @property
121
+ def high(self):
122
+ return self.__full_pointer[1]
123
+
124
+ @high.setter
125
+ def high(self, value):
126
+ self.__full_pointer[1] = self.integer_or_hex(value)
127
+
128
+ @property
129
+ def bank(self):
130
+ return self.__full_pointer[2]
131
+
132
+ @bank.setter
133
+ def bank(self, value):
134
+ self.__full_pointer[2] = self.integer_or_hex(value)
135
+
136
+ def to_addr(self, addr_type):
137
+ return SFCAddress(self.full_address, addr_type)
138
+
139
+ @staticmethod
140
+ def __check_list_tuple(value):
141
+ if not (type(value) is list or type(value) is tuple):
142
+ raise ValueError("Cannot assign any value but type of list or tuple.")
143
+ if not len(value) >= 1:
144
+ raise ValueError("List/Tuple length must be at least 1.")
145
+
146
+ def __set_ptr_pos(self, index, input_val):
147
+ if len(input_val) > index:
148
+ self.__full_pointer[index] = self.integer_or_hex(input_val[index])
149
+
150
+ @staticmethod
151
+ def integer_or_hex(value: Union[int, str], mask: int = 0xFF) -> int:
152
+ """
153
+ validation for input values, also applies masking to the value
154
+ :param value:
155
+ :param mask: value is logical and'ed to the mask
156
+ :return: normalized, and masked integer
157
+ """
158
+ if type(value) is str:
159
+ if value.upper().startswith('0X'):
160
+ value = value.replace('0X', '')
161
+ try:
162
+ value = int(value, 16)
163
+ except ValueError as ex:
164
+ raise ValueError('`address` parameter must be an integer or a hexadecimal string!')
165
+ elif type(value) is not int:
166
+ raise ValueError('`address` parameter must be an integer or a hexadecimal string!')
167
+ return value & mask
168
+
169
+
170
+ class SFCAddressType:
171
+ PC = 0
172
+ LOROM1 = 1
173
+ LOROM2 = 2
174
+ HIROM = 3
175
+ EXHIROM = 4
176
+ EXLOROM = 5
177
+
178
+
179
+ class SFCAddress:
180
+ def __init__(self, address: Union[int, str, list, tuple], address_type: int = SFCAddressType.PC,
181
+ default_value='N/A', hex_prefix='0x', decimal: bool = False, header: bool = False,
182
+ verbose=False, lorom_fallback=True):
183
+ """
184
+ Class can be instantiated in case multiple conversions are desired.
185
+ :param address: integer/hexadecimal address value
186
+ :param address_type: the input address type-- defaults to PC
187
+ :param default_value: the value that is shown while printing values if the conversion failed
188
+ :param hex_prefix: this string is prepended to the output hex value-- defaults to 0x (ex: 0x0BC018)
189
+ :param decimal: boolean value to indicate the default conversion output value-- defaults to False
190
+ :param header: indicate whether the conversion should take a copier header into account-- default False
191
+ :param verbose: if more console output is desired-- default False
192
+ :param lorom_fallback: if LoROM 1/2 conversion fails, they will fall back to the other type
193
+ """
194
+ self.__header = header
195
+ self.__prefix = hex_prefix
196
+ self.__show_hex = not decimal
197
+ self.__default = default_value
198
+ self.__verbose = verbose
199
+ self.__lorom_fallback = lorom_fallback
200
+ self.__initial_type = address_type
201
+
202
+ if type(address) is not list and type(address) is not tuple:
203
+ address = SFCPointer.integer_or_hex(address, 0xFFFFFF)
204
+ else:
205
+ ptr = SFCPointer(*address)
206
+ address = ptr.full_address
207
+
208
+ self.__given_address = address
209
+
210
+ if address_type == SFCAddressType.PC:
211
+ self.__address = address if not header else header - 512
212
+ elif address_type == SFCAddressType.LOROM1:
213
+ self.__address = self.lorom1_to_pc(address, self.__verbose, self.__lorom_fallback)
214
+ elif address_type == SFCAddressType.LOROM2:
215
+ self.__address = self.lorom2_to_pc(address, self.__verbose, self.__lorom_fallback)
216
+ elif address_type == SFCAddressType.HIROM:
217
+ self.__address = self.hirom_to_pc(address)
218
+ elif address_type == SFCAddressType.EXHIROM:
219
+ self.__address = self.exhirom_to_pc(address)
220
+ elif address_type == SFCAddressType.EXLOROM:
221
+ self.__address = self.exlorom_to_pc(address)
222
+ else:
223
+ raise ValueError('`address_type` parameter is invalid!')
224
+ if verbose:
225
+ print(self.all())
226
+
227
+ def all(self):
228
+ """
229
+ Return a text representation of the current object using all possible conversions
230
+ """
231
+ hirom = self.hirom_address
232
+ exhirom = self.exhirom_address
233
+ lorom = self.lorom1_address
234
+ exlorom = self.exlorom_address
235
+ lorom2 = self.lorom2_address
236
+ my_repr = f"=====TYPE====:=ADDRESS=\r\n****Binary/PC: {self.pc_address}"
237
+ if lorom == exlorom:
238
+ if lorom2 == lorom:
239
+ my_repr += f"\r\n(1/2/Ex)LoROM: {lorom}"
240
+ else:
241
+ my_repr += f"\r\n**(1/Ex)LoROM: {lorom}\r\n*****(2)LoROM: {lorom2}"
242
+ elif lorom2 == exlorom:
243
+ my_repr += f"\r\n*****(1)LoROM: {lorom}\r\n**(2/Ex)LoROM: {lorom2}"
244
+ else:
245
+ my_repr += f"\r\n*****(1)LoROM: {lorom}\r\n*****(2)LoROM: {lorom2}\r\n******ExLoROM: {exlorom}"
246
+
247
+ my_repr += f"\r\n*****Ex/HiROM: {hirom}" if hirom == exhirom else \
248
+ f"\r\n********HiROM: {hirom}\r\n******ExHiROM: {exhirom}"
249
+
250
+ return my_repr
251
+
252
+ def __str__(self):
253
+ return self.display_address(self.get_address(self.__initial_type))
254
+
255
+ def __repr__(self):
256
+ return f"{self.pc_address}({self.__address})"
257
+
258
+ @lru_cache(0xFFFFFF)
259
+ def display_address(self, addr, fill_hex_length=True, show_prefix=True):
260
+ if addr is not None:
261
+ if self.__show_hex:
262
+ addr = hex(addr).upper().replace('0X', '')
263
+ if fill_hex_length:
264
+ while len(addr) < 6:
265
+ addr = f"0{addr}"
266
+ if show_prefix:
267
+ addr = f"{self.__prefix}{addr}"
268
+ return addr
269
+ return self.__default
270
+
271
+ @lru_cache(0xFFFFFF)
272
+ def get_address(self, address_type: Optional[int] = None) -> int:
273
+ addr = 0
274
+ if address_type is None:
275
+ address_type = self.__initial_type
276
+
277
+ if address_type == SFCAddressType.PC:
278
+ addr = self.__address
279
+ elif address_type == SFCAddressType.LOROM1:
280
+ addr = self.pc_to_lorom1(self.__address)
281
+ elif address_type == SFCAddressType.LOROM2:
282
+ addr = self.pc_to_lorom2(self.__address)
283
+ elif address_type == SFCAddressType.HIROM:
284
+ addr = self.pc_to_hirom(self.__address)
285
+ elif address_type == SFCAddressType.EXHIROM:
286
+ addr = self.pc_to_exhirom(self.__address)
287
+ elif address_type == SFCAddressType.EXLOROM:
288
+ addr = self.pc_to_exlorom(self.__address)
289
+ return addr
290
+
291
+ def to_pointer(self, addr=None):
292
+ if addr is None:
293
+ addr = self.__address
294
+ return SFCPointer(addr)
295
+
296
+ @lru_cache(0xFFFFFF)
297
+ def get_address_bytes(self, address_type: Optional[SFCAddressType] = None) -> list:
298
+ return [self.get_low_byte(address_type), self.get_high_byte(address_type), self.get_bank_byte(address_type)]
299
+
300
+ @lru_cache(0xFFFFFF)
301
+ def get_low_byte(self, address_type: Optional[int] = None) -> int:
302
+ addr = self.get_address(address_type)
303
+ return self.low_byte(addr)
304
+
305
+ @staticmethod
306
+ @lru_cache(0xFFFFFF)
307
+ def low_byte(addr: int):
308
+ """
309
+ Return a single (lowest) byte for a given address
310
+ """
311
+ return addr & 0xFF
312
+
313
+ @lru_cache(0xFFFFFF)
314
+ def get_high_byte(self, address_type: Optional[int] = None) -> int:
315
+ addr = self.get_address(address_type)
316
+ return self.high_byte(addr)
317
+
318
+ @staticmethod
319
+ @lru_cache(0xFFFFFF)
320
+ def high_byte(addr: int):
321
+ return int(addr / 0x100) & 0xFF
322
+
323
+ @lru_cache(0xFFFFFF)
324
+ def get_bank_byte(self, address_type: Optional[int] = None) -> int:
325
+ addr = self.get_address(address_type)
326
+ return self.bank_byte(addr)
327
+
328
+ @staticmethod
329
+ @lru_cache(0xFFFFFF)
330
+ def bank_byte(addr: int):
331
+ return int(addr / 0x10000) & 0xFF
332
+
333
+ @property
334
+ @lru_cache(0xFFFFFF)
335
+ def pc_address(self):
336
+ return self.display_address(self.__address if not self.__header else self.__address + 512)
337
+
338
+ @property
339
+ @lru_cache(0xFFFFFF)
340
+ def lorom1_address(self):
341
+ return self.display_address(self.pc_to_lorom1(self.__address))
342
+
343
+ @property
344
+ @lru_cache(0xFFFFFF)
345
+ def lorom2_address(self):
346
+ return self.display_address(self.pc_to_lorom2(self.__address))
347
+
348
+ @property
349
+ @lru_cache(0xFFFFFF)
350
+ def exlorom_address(self):
351
+ return self.display_address(self.pc_to_exlorom(self.__address))
352
+
353
+ @property
354
+ @lru_cache(0xFFFFFF)
355
+ def hirom_address(self):
356
+ return self.display_address(self.pc_to_hirom(self.__address))
357
+
358
+ @property
359
+ @lru_cache(0xFFFFFF)
360
+ def exhirom_address(self):
361
+ return self.display_address(self.pc_to_exhirom(self.__address))
362
+
363
+ @classmethod
364
+ @lru_cache(0xFFFFFF)
365
+ def pc_to_lorom1(cls, pc_addr: int, verbose: bool = False) -> Optional[int]:
366
+ if pc_addr is None:
367
+ if verbose:
368
+ print("pc_to_lorom1: Given Address is invalid.")
369
+ return None
370
+ if pc_addr >= 0x400000:
371
+ return None
372
+
373
+ snes_addr = ((pc_addr << 1) & 0x7F0000) | ((pc_addr | 0x8000) & 0xFFFF)
374
+
375
+ if pc_addr >= 0x380000:
376
+ snes_addr += 0x800000
377
+
378
+ return snes_addr
379
+
380
+ @classmethod
381
+ @lru_cache(0xFFFFFF)
382
+ def pc_to_lorom2(cls, pc_addr: int, verbose: bool = False) -> Optional[int]:
383
+ if pc_addr is None:
384
+ if verbose:
385
+ print("pc_to_lorom2: Given Address is invalid.")
386
+ return None
387
+ if pc_addr >= 0x400000:
388
+ return None
389
+
390
+ return (((pc_addr << 1) & 0x7F0000) | ((pc_addr | 0x8000) & 0xFFFF)) + 0x800000
391
+
392
+ @classmethod
393
+ @lru_cache(0xFFFFFF)
394
+ def pc_to_hirom(cls, pc_addr: int, verbose: bool = False) -> Optional[int]:
395
+ if pc_addr is None:
396
+ if verbose:
397
+ print("pc_to_hirom: Given Address is invalid.")
398
+ return None
399
+ if pc_addr >= 0x400000:
400
+ return None
401
+
402
+ return pc_addr | 0xC00000
403
+
404
+ @classmethod
405
+ @lru_cache(0xFFFFFF)
406
+ def pc_to_exlorom(cls, pc_addr: int, verbose: bool = False) -> Optional[int]:
407
+ if pc_addr is None:
408
+ if verbose:
409
+ print("pc_to_exlorom: Given Address is invalid.")
410
+ return None
411
+ if pc_addr >= 0x7F0000:
412
+ return None
413
+
414
+ snes_addr = ((pc_addr << 1) & 0x7F0000) | ((pc_addr | 0x8000) & 0xFFFF)
415
+
416
+ if pc_addr < 0x400000:
417
+ snes_addr += 0x800000
418
+
419
+ return snes_addr
420
+
421
+ @classmethod
422
+ @lru_cache(0xFFFFFF)
423
+ def pc_to_exhirom(cls, pc_addr: int, verbose: bool = False) -> Optional[int]:
424
+ if pc_addr is None:
425
+ if verbose:
426
+ print("pc_to_exhirom: Given Address is invalid.")
427
+ return None
428
+ if pc_addr >= 0x7E0000:
429
+ return None
430
+
431
+ snes_addr = pc_addr
432
+ if pc_addr < 0x400000:
433
+ snes_addr |= 0xC00000
434
+ # elif pc_addr >= 0x7E0000:
435
+ # snes_addr -= 0x400000
436
+
437
+ return snes_addr
438
+
439
+ @classmethod
440
+ @lru_cache(0xFFFFFF)
441
+ def lorom1_to_pc(cls, snes_addr: int, verbose: bool = True, fallback=False) -> Optional[int]:
442
+ if snes_addr is None:
443
+ if verbose:
444
+ print("lorom1_to_pc: Given Address is invalid.")
445
+ return None
446
+ if not (0x8000 <= snes_addr <= 0x6FFFFF):
447
+ if verbose:
448
+ print("Not a valid LoROM1 address!")
449
+ return cls.lorom2_to_pc(snes_addr, verbose) if fallback else None
450
+
451
+ return snes_addr & 0x7FFF | ((snes_addr & 0x7F0000) >> 1)
452
+
453
+ @classmethod
454
+ @lru_cache(0xFFFFFF)
455
+ def lorom2_to_pc(cls, snes_addr: int, verbose: bool = True, fallback=False) -> Optional[int]:
456
+ if snes_addr is None:
457
+ if verbose:
458
+ print("lorom2_to_pc: Given Address is invalid.")
459
+ return None
460
+ if not (0x808000 <= snes_addr <= 0xFFFFFF):
461
+ if verbose:
462
+ print("Not a valid LoROM2 address!")
463
+ return cls.lorom1_to_pc(snes_addr, verbose) if fallback else None
464
+
465
+ return snes_addr & 0x7FFF | ((snes_addr & 0x7F0000) >> 1)
466
+
467
+ @classmethod
468
+ @lru_cache(0xFFFFFF)
469
+ def hirom_to_pc(cls, snes_addr: int, verbose: bool = False) -> Optional[int]:
470
+ if snes_addr is None:
471
+ if verbose:
472
+ print("hirom_to_pc: Given Address is invalid.")
473
+ return None
474
+
475
+ if not (0xC00000 <= snes_addr <= 0xFFFFFF):
476
+ print("Invalid HiROM Address!")
477
+ return None
478
+
479
+ return snes_addr & 0x3FFFFF
480
+
481
+ @classmethod
482
+ @lru_cache(0xFFFFFF)
483
+ def exlorom_to_pc(cls, snes_addr: int, verbose: bool = False) -> Optional[int]:
484
+ if snes_addr is None:
485
+ if verbose:
486
+ print("exlorom_to_pc: Given Address is invalid.")
487
+ return None
488
+ if not ((0x808000 <= snes_addr <= 0xFFFFFF) or (0x008000 <= snes_addr <= 0x7DFFFF)):
489
+ print("Invalid ExLoROM Address!")
490
+ return None
491
+
492
+ pc_addr = snes_addr & 0x7FFF | ((snes_addr & 0x7F0000) >> 1)
493
+
494
+ if snes_addr < 0x800000:
495
+ pc_addr += 0x400000
496
+
497
+ return pc_addr
498
+
499
+ @classmethod
500
+ @lru_cache(0xFFFFFF)
501
+ def exhirom_to_pc(cls, snes_addr: int, verbose: bool = False) -> Optional[int]:
502
+ if snes_addr is None:
503
+ if verbose:
504
+ print("exhirom_to_pc: Given Address is invalid.")
505
+ return None
506
+ if not ((0xC00000 <= snes_addr <= 0xFFFFFF) or (0x400000 <= snes_addr <= 0x7DFFFF)):
507
+ print("Invalid ExHiROM Address!")
508
+ return None
509
+
510
+ pc_addr = snes_addr & 0x3FFFFF
511
+ if snes_addr < 0xC00000:
512
+ pc_addr += 0x400000
513
+
514
+ return pc_addr
515
+
516
+
517
+ def lorom_to_hirom(in_data: list):
518
+ """
519
+ Converts a full binary rom file (list of bytes) from lorom to hirom format (doubles every bank)
520
+ Quick and dirty.
521
+ :param in_data: the original data
522
+ :return: the hirom data
523
+ """
524
+ final_data = [0xFF] * (len(in_data) * 2)
525
+
526
+ div = 0x8000
527
+ pcs = int(len(in_data) / div)
528
+
529
+ for c in range(0, pcs):
530
+ for d in range(0, div):
531
+ pc_pos = d + (c * div)
532
+ hirom_pos = d + (c * 0x10000)
533
+ final_data[hirom_pos] = 0xFF if c == 0 else in_data[pc_pos]
534
+ final_data[hirom_pos + div] = in_data[pc_pos]
535
+ return final_data
536
+
537
+
538
+ def run_test(function1, function2, i, name, verbose_progress, **kwargs):
539
+ addr = function1(i)
540
+ conv = function2(addr, **kwargs) if addr else 0
541
+ if verbose_progress and addr:
542
+ print(f"{hex(i)}->{hex(addr)}->{hex(conv) if conv is not None else 'ERR'} - {name}")
543
+ if addr and not i == conv:
544
+ print(f"{hex(i)}->{hex(addr)}->{hex(conv) if conv is not None else 'ERR'}"
545
+ f" - {name} Back-Conversion Failed!")
546
+ return False
547
+ return True
548
+
549
+
550
+ def test_conv(start=0, end=0x7FFFFF, step=0x8000, verbose=True, stop_on_failure=True, lorom1_kwargs=None):
551
+ fail_count = 0
552
+ lorom1_kwargs = lorom1_kwargs if lorom1_kwargs else {'fallback': True, 'verbose': True}
553
+ for i in range(start, end, step):
554
+ if not run_test(SFCAddress.pc_to_lorom1, SFCAddress.lorom1_to_pc, i, "LOROM1", verbose,
555
+ **lorom1_kwargs):
556
+ fail_count += 1
557
+
558
+ if not run_test(SFCAddress.pc_to_lorom2, SFCAddress.lorom2_to_pc, i, "LOROM2", verbose):
559
+ fail_count += 1
560
+
561
+ if not run_test(SFCAddress.pc_to_hirom, SFCAddress.hirom_to_pc, i, "HIROM", verbose):
562
+ fail_count += 1
563
+
564
+ if not run_test(SFCAddress.pc_to_exlorom, SFCAddress.exlorom_to_pc, i, "EXLOROM", verbose):
565
+ fail_count += 1
566
+
567
+ if not run_test(SFCAddress.pc_to_exhirom, SFCAddress.exhirom_to_pc, i, "EXHIROM", verbose):
568
+ fail_count += 1
569
+
570
+ if stop_on_failure and fail_count > 0:
571
+ break
572
+ if verbose:
573
+ print("...")
574
+
575
+ if not fail_count:
576
+ print("ALL TESTS PASSED!")
577
+ else:
578
+ print(f"ENCOUNTERED {fail_count} FAILURES!")
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.2
2
+ Name: retrotool
3
+ Version: 0.1.0
4
+ Summary: Address Conversion Tool for the Super Famicom
5
+ Author-email: Daniel Burgess <daniel@herotechsys.com>
6
+ License: This is free and unencumbered software released into the public domain.
7
+
8
+ Anyone is free to copy, modify, publish, use, compile, sell, or
9
+ distribute this software, either in source code form or as a compiled
10
+ binary, for any purpose, commercial or non-commercial, and by any
11
+ means.
12
+
13
+ In jurisdictions that recognize copyright laws, the author or authors
14
+ of this software dedicate any and all copyright interest in the
15
+ software to the public domain. We make this dedication for the benefit
16
+ of the public at large and to the detriment of our heirs and
17
+ successors. We intend this dedication to be an overt act of
18
+ relinquishment in perpetuity of all present and future rights to this
19
+ software under copyright law.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
25
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
26
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27
+ OTHER DEALINGS IN THE SOFTWARE.
28
+
29
+ For more information, please refer to <https://unlicense.org>
30
+
31
+ Project-URL: Homepage, https://github.com/danielburgess/SFCRetroTools
32
+ Requires-Python: >=3.8
33
+ License-File: LICENSE
34
+ Requires-Dist: chardet>=5.2.0
@@ -0,0 +1,7 @@
1
+ retrotool/script.py,sha256=xq66F4uMzga0_KxFIivkIbk34yb-mryPi68UI8Wnhio,11706
2
+ retrotool/snes.py,sha256=A7_HqFu7yf4v5e3ImwsTRDROso27s5_oKNRl0ZnkBKU,20732
3
+ retrotool-0.1.0.dist-info/LICENSE,sha256=Q2ptU2E48gOsMzhYz_kqVovmJ5d1KzrblLyqD2_-fvY,1235
4
+ retrotool-0.1.0.dist-info/METADATA,sha256=nCsfSaGMUKBcb-RVQXbgLlk1J1k2CGIayzxstbKO570,1754
5
+ retrotool-0.1.0.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
6
+ retrotool-0.1.0.dist-info/top_level.txt,sha256=fvpiw_pQVbUWzZgMrHEpS8i4VHEaqEoz-niMqpGXjPc,10
7
+ retrotool-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ retrotool