retrotool 0.1.0__tar.gz
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-0.1.0/LICENSE +24 -0
- retrotool-0.1.0/PKG-INFO +34 -0
- retrotool-0.1.0/README.md +198 -0
- retrotool-0.1.0/pyproject.toml +16 -0
- retrotool-0.1.0/retrotool/script.py +313 -0
- retrotool-0.1.0/retrotool/snes.py +578 -0
- retrotool-0.1.0/retrotool.egg-info/PKG-INFO +34 -0
- retrotool-0.1.0/retrotool.egg-info/SOURCES.txt +10 -0
- retrotool-0.1.0/retrotool.egg-info/dependency_links.txt +1 -0
- retrotool-0.1.0/retrotool.egg-info/requires.txt +1 -0
- retrotool-0.1.0/retrotool.egg-info/top_level.txt +1 -0
- retrotool-0.1.0/setup.cfg +4 -0
retrotool-0.1.0/LICENSE
ADDED
|
@@ -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>
|
retrotool-0.1.0/PKG-INFO
ADDED
|
@@ -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,198 @@
|
|
|
1
|
+
# SFCRetroTools
|
|
2
|
+
#### ===ROM Hacking Tools for the Super Famicom===
|
|
3
|
+
|
|
4
|
+
This repo will contain various libraries I've created for my personal use.
|
|
5
|
+
|
|
6
|
+
Currently, this only contains an Address Conversion tool, and a Pointer class which are used similarly to LunarAddress, except I don't support ZSNES save states. I could, but... does anyone even use ZSNES anymore?
|
|
7
|
+
|
|
8
|
+
### Supported Address Mapping Conversions:
|
|
9
|
+
* LoROM (Type 1/2)
|
|
10
|
+
* HiROM
|
|
11
|
+
* ExLoROM
|
|
12
|
+
* ExHiROM
|
|
13
|
+
* PC/Binary
|
|
14
|
+
|
|
15
|
+
### Basic Usage:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from retrotool.snes import SFCAddress, SFCAddressType
|
|
19
|
+
|
|
20
|
+
addr = SFCAddress(0x5f800, SFCAddressType.PC)
|
|
21
|
+
print(addr.all()) # all applicable conversions are shown
|
|
22
|
+
print(addr.exhirom_address) # hex-formatted EXHIROM address
|
|
23
|
+
```
|
|
24
|
+
```text
|
|
25
|
+
=====TYPE====:=ADDRESS=
|
|
26
|
+
****Binary/PC: 0x05F800
|
|
27
|
+
*****(1)LoROM: 0x0BF800
|
|
28
|
+
**(2/Ex)LoROM: 0x8BF800
|
|
29
|
+
*****Ex/HiROM: 0xC5F800
|
|
30
|
+
'0xC5F800'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Short Explainer
|
|
34
|
+
There are several helper methods that can be used for converting SNES/SFC addresses. Essentially, this library can be combined with any number of other tools such as script dumping, pointer table generation, address conversions built in to hex editors, etc.
|
|
35
|
+
|
|
36
|
+
=======
|
|
37
|
+
I plan on adding more tools as I need it, but in the meantime,
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
# SFCPointer Class
|
|
42
|
+
|
|
43
|
+
## Description
|
|
44
|
+
The `SFCPointer` class represents a Super Famicom (SFC) pointer. Pointers can be defined, modified, and read in various ways. The class allows you to specify low, high, and bank values, and it provides methods for validation, conversion, and display.
|
|
45
|
+
|
|
46
|
+
## Constructor
|
|
47
|
+
|
|
48
|
+
### `init(self, low=None, high=None, bank=None)`
|
|
49
|
+
|
|
50
|
+
Pointers can be defined, modified, and read in many ways. Low and high values can be used to fill part of, or the entire address, depending on what is desired.
|
|
51
|
+
|
|
52
|
+
- `low`: Can be as small as 8 bit, as big as 24 bit (over 24 bit is ignored).
|
|
53
|
+
- `high`: Can be 8 to 16 bit (over 16 bit is ignored).
|
|
54
|
+
- `bank`: Only be 8 bit (extra data is lost).
|
|
55
|
+
|
|
56
|
+
## Methods
|
|
57
|
+
|
|
58
|
+
### `validate_bytes(cls, *args)`
|
|
59
|
+
Validates and normalizes low, high, and bank values.
|
|
60
|
+
- `args`: Low, high, and bank values.
|
|
61
|
+
Returns a tuple `(low, high, bank)`.
|
|
62
|
+
|
|
63
|
+
### `hex_fmt(value, pad=4, prefix='0x')`
|
|
64
|
+
Formats an integer value as a hexadecimal string.
|
|
65
|
+
- `value`: Integer value to be formatted.
|
|
66
|
+
- `pad`: Width of the formatted string (default is 4).
|
|
67
|
+
- `prefix`: Prefix for the hexadecimal string (default is '0x').
|
|
68
|
+
Returns the formatted hexadecimal string.
|
|
69
|
+
|
|
70
|
+
### `to_addr(addr_type)`
|
|
71
|
+
Converts the `SFCPointer` to an `SFCAddress` instance.
|
|
72
|
+
- `addr_type`: The address type to convert to.
|
|
73
|
+
Returns an `SFCAddress` instance.
|
|
74
|
+
|
|
75
|
+
### `integer_or_hex(value: Union[int, str], mask: int = 0xFF) -> int`
|
|
76
|
+
Validates and normalizes input values, applying masking to the value.
|
|
77
|
+
- `value`: Input value (integer or hexadecimal string).
|
|
78
|
+
- `mask`: Mask to be applied (default is 0xFF).
|
|
79
|
+
Returns the normalized and masked integer value.
|
|
80
|
+
|
|
81
|
+
### `__set_ptr_pos(self, index, input_val)`
|
|
82
|
+
Sets the value at the specified index in the full pointer.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
# SFCAddressType Class
|
|
87
|
+
|
|
88
|
+
## Description
|
|
89
|
+
The `SFCAddressType` class defines constants representing different Super Famicom (SFC) address types.
|
|
90
|
+
|
|
91
|
+
## Constants
|
|
92
|
+
- `PC`: Address type for PC addresses.
|
|
93
|
+
- `LOROM1`: Address type for LoROM1 addresses.
|
|
94
|
+
- `LOROM2`: Address type for LoROM2 addresses.
|
|
95
|
+
- `HIROM`: Address type for HiROM addresses.
|
|
96
|
+
- `EXHIROM`: Address type for ExHiROM addresses.
|
|
97
|
+
- `EXLOROM`: Address type for ExLoROM addresses.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
# SFCAddress Class
|
|
102
|
+
|
|
103
|
+
## Description
|
|
104
|
+
The `SFCAddress` class provides a flexible way to handle Super Famicom (SFC) addresses. It allows for instantiation with various input types and supports multiple conversions between different address types.
|
|
105
|
+
|
|
106
|
+
## Constructor
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
__init__(self, address: Union[int, str, list, tuple], address_type: int = SFCAddressType.PC,
|
|
110
|
+
default_value='N/A', hex_prefix='0x', decimal: bool = False, header: bool = False,
|
|
111
|
+
verbose=False, lorom_fallback=True)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- `address`: Integer, hexadecimal string, list, or tuple representing the address value.
|
|
115
|
+
- `address_type`: The input address type (default is SFCAddressType.PC).
|
|
116
|
+
- `default_value`: The value shown while printing if the conversion fails (default is 'N/A').
|
|
117
|
+
- `hex_prefix`: String prepended to the output hex value (default is '0x').
|
|
118
|
+
- `decimal`: Boolean indicating the default conversion output value (default is False).
|
|
119
|
+
- `header`: Indicates whether the conversion should consider a copier header (default is False).
|
|
120
|
+
- `verbose`: If more console output is desired (default is False).
|
|
121
|
+
- `lorom_fallback`: If LoROM 1/2 conversion fails, it will fall back to the other type.
|
|
122
|
+
|
|
123
|
+
## Properties and Methods
|
|
124
|
+
|
|
125
|
+
### `all(self) -> str`
|
|
126
|
+
Prints a formatted representation of the address in various SFC address types.
|
|
127
|
+
|
|
128
|
+
### `display_address(self, addr, fill_hex_length=True, show_prefix=True) -> Union[str, int]`
|
|
129
|
+
Formats and displays the given address.
|
|
130
|
+
|
|
131
|
+
### `get_address(self, address_type: Optional[int] = None) -> int`
|
|
132
|
+
Returns the address in the specified type.
|
|
133
|
+
|
|
134
|
+
### `to_pointer(self, addr=None) -> SFCPointer`
|
|
135
|
+
Converts the given address to an SFCPointer object.
|
|
136
|
+
|
|
137
|
+
### `get_address_bytes(self, address_type: Optional[SFCAddressType] = None) -> list`
|
|
138
|
+
Returns a list of low, high, and bank bytes of the address.
|
|
139
|
+
|
|
140
|
+
### `get_low_byte(self, address_type: Optional[int] = None) -> int`
|
|
141
|
+
Returns the low byte of the address.
|
|
142
|
+
|
|
143
|
+
### `get_high_byte(self, address_type: Optional[int] = None) -> int`
|
|
144
|
+
Returns the high byte of the address.
|
|
145
|
+
|
|
146
|
+
### `get_bank_byte(self, address_type: Optional[int] = None) -> int`
|
|
147
|
+
Returns the bank byte of the address.
|
|
148
|
+
|
|
149
|
+
### `pc_address(self) -> str`
|
|
150
|
+
Returns the address in PC format.
|
|
151
|
+
|
|
152
|
+
### `lorom1_address(self) -> str`
|
|
153
|
+
Returns the address in LoROM1 format.
|
|
154
|
+
|
|
155
|
+
### `lorom2_address(self) -> str`
|
|
156
|
+
Returns the address in LoROM2 format.
|
|
157
|
+
|
|
158
|
+
### `exlorom_address(self) -> str`
|
|
159
|
+
Returns the address in ExLoROM format.
|
|
160
|
+
|
|
161
|
+
### `hirom_address(self) -> str`
|
|
162
|
+
Returns the address in HiROM format.
|
|
163
|
+
|
|
164
|
+
### `exhirom_address(self) -> str`
|
|
165
|
+
Returns the address in ExHiROM format.
|
|
166
|
+
|
|
167
|
+
## Class Methods
|
|
168
|
+
|
|
169
|
+
### `pc_to_lorom1(cls, pc_addr: int, verbose: bool = False) -> Optional[int]`
|
|
170
|
+
Converts a PC address to LoROM1 format.
|
|
171
|
+
|
|
172
|
+
### `pc_to_lorom2(cls, pc_addr: int, verbose: bool = False) -> Optional[int]`
|
|
173
|
+
Converts a PC address to LoROM2 format.
|
|
174
|
+
|
|
175
|
+
### `pc_to_hirom(cls, pc_addr: int, verbose: bool = False) -> Optional[int]`
|
|
176
|
+
Converts a PC address to HiROM format.
|
|
177
|
+
|
|
178
|
+
### `pc_to_exlorom(cls, pc_addr: int, verbose: bool = False) -> Optional[int]`
|
|
179
|
+
Converts a PC address to ExLoROM format.
|
|
180
|
+
|
|
181
|
+
### `pc_to_exhirom(cls, pc_addr: int, verbose: bool = False) -> Optional[int]`
|
|
182
|
+
Converts a PC address to ExHiROM format.
|
|
183
|
+
|
|
184
|
+
### `lorom1_to_pc(cls, snes_addr: int, verbose: bool = True, fallback=False) -> Optional[int]`
|
|
185
|
+
Converts a LoROM1 address to PC format.
|
|
186
|
+
|
|
187
|
+
### `lorom2_to_pc(cls, snes_addr: int, verbose: bool = True, fallback=False) -> Optional[int]`
|
|
188
|
+
Converts a LoROM2 address to PC format.
|
|
189
|
+
|
|
190
|
+
### `hirom_to_pc(cls, snes_addr: int1, verbose: bool = False) -> Optional[int]`
|
|
191
|
+
Converts a HiROM address to PC format.
|
|
192
|
+
|
|
193
|
+
### `exlorom_to_pc(cls, snes_addr: int, verbose: bool = False) -> Optional[int]`
|
|
194
|
+
Converts an ExLoROM address to PC format.
|
|
195
|
+
|
|
196
|
+
### `exhirom_to_pc(cls, snes_addr: int, verbose: bool = False) -> Optional[int]`
|
|
197
|
+
Converts an ExHiROM address to PC format.
|
|
198
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "retrotool"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Address Conversion Tool for the Super Famicom"
|
|
5
|
+
authors = [{name = "Daniel Burgess", email = "daniel@herotechsys.com"}]
|
|
6
|
+
license = {file = "LICENSE"}
|
|
7
|
+
dependencies = ["chardet>=5.2.0"] # If your package needs other libraries
|
|
8
|
+
requires-python = ">=3.8"
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
12
|
+
build-backend = "setuptools.build_meta"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
"Homepage" = "https://github.com/danielburgess/SFCRetroTools"
|
|
@@ -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")
|
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chardet>=5.2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
retrotool
|