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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
|
# SPDX-FileCopyrightText: © 2025 ZeldaRET
# SPDX-License-Identifier: CC0-1.0
import abc
from dataclasses import dataclass
from typing import Callable, TypeVar, Generic
try:
from rich.pretty import pprint as rich_pprint
except ImportError:
rich_pprint = print
from . import Resource, File, GetResourceAtResult
# when failing to resolve an address,
# (try to) keep going by creating "fake" files/resources
# or defaulting to poor choices (e.g. raw addresses)
BEST_EFFORT = True
if BEST_EFFORT:
VERBOSE_BEST_EFFORT = 1
VERBOSE_BEST_EFFORT_LVL1_IGNORED_SEGS = {
1, # billboard matrix segment
8, # often used for eye/mouth textures, or various dlist callbacks. same for 9-0xC
9,
0xA,
0xB,
0xC,
0xD, # matrix buffer for skeletons dlists
}
# RangeMap
RangeMapValueT = TypeVar("RangeMapValueT")
class RangeMap(Generic[RangeMapValueT]):
def __init__(self):
self.values_by_range: dict[tuple[int, int], RangeMapValueT] = dict()
def set(self, range_start: int, range_end: int, value: RangeMapValueT):
assert range_start < range_end
current_values_in_range = self.get_all_in_range(range_start, range_end)
if current_values_in_range:
raise Exception(
"Range already used (at least partially)",
hex(range_start),
hex(range_end),
current_values_in_range,
)
self.values_by_range[(range_start, range_end)] = value
def get_all_by_predicate(self, predicate: Callable[[int, int], bool]):
"""Return all values associated to a range for which the predicate returns True"""
values: dict[tuple[int, int], RangeMapValueT] = dict()
for (range_start, range_end), value in self.values_by_range.items():
if predicate(range_start, range_end):
values[(range_start, range_end)] = value
return values
def get_all_in_range(self, range_start: int, range_end: int):
"""Return all values associated to a range intersecting with the given range"""
assert range_start < range_end
def check_intersect(value_range_start, value_range_end):
assert value_range_start < value_range_end
if range_end <= value_range_start:
return False
if value_range_end <= range_start:
return False
return True
return self.get_all_by_predicate(check_intersect)
def get(self, offset) -> RangeMapValueT:
"""Return the value associated to the range the given offset is in,
if any, or raise IndexError"""
def check_belong(value_range_start, value_range_end):
assert value_range_start < value_range_end
return value_range_start <= offset < value_range_end
values = self.get_all_by_predicate(check_belong)
assert len(values) <= 1, values
if values:
return next(iter(values.values()))
else:
raise IndexError(offset)
def copy(self):
"""Returns a shallow copy"""
other = RangeMap()
other.values_by_range = self.values_by_range.copy()
return other
class NoResourceError(Exception):
"""There is no resource at the requested address"""
pass
class UnexpectedResourceTypeError(Exception):
"""There is a resource at the requested address, but of the wrong type"""
pass
class UnmappedAddressError(Exception):
"""Indicates an address could not be resolved because nothing was found for the address."""
pass
AttributeValueT = TypeVar("AttributeValueT")
@dataclass(frozen=True)
class Attribute(Generic[AttributeValueT]):
name: str # Uniquely identifies the attribute
value_type: type[AttributeValueT]
def __eq__(self, other):
if isinstance(other, Attribute):
return self.name == other.name
else:
return False
def __hash__(self):
return hash(self.name)
class Attributes:
c_reference = Attribute("c_reference", str)
c_expression_length = Attribute("c_expression_length", str)
ResourceT = TypeVar("ResourceT", bound="Resource")
class AddressResolveResult:
def __init__(self, original_address: int, file: File, file_offset: int):
self.original_address = original_address
"""Original address that was resolved to this result (for debugging purposes)"""
self.file = file
self.file_offset = file_offset
def get_resource(self, resource_type: type[ResourceT]) -> ResourceT:
result, resource = self.file.get_resource_at(self.file_offset)
if result != GetResourceAtResult.DEFINITIVE:
raise NoResourceError("No definitive resource", result)
assert resource is not None
if resource.range_start != self.file_offset:
raise NoResourceError(
"No resource at (exactly) the requested address", resource
)
if not isinstance(resource, resource_type):
raise UnexpectedResourceTypeError(resource, resource_type)
return resource
def get_attribute(self, attribute: Attribute[AttributeValueT]) -> AttributeValueT:
result, resource = self.file.get_resource_at(self.file_offset)
if result != GetResourceAtResult.DEFINITIVE:
raise Exception("No definitive resource", result)
assert resource is not None
resource_offset = self.file_offset - resource.range_start
if attribute == Attributes.c_reference:
value = resource.get_c_reference(resource_offset)
elif attribute == Attributes.c_expression_length:
value = resource.get_c_expression_length(resource_offset)
else:
raise NotImplementedError(attribute)
if not isinstance(value, attribute.value_type):
raise Exception(
"Resource gave an attribute value of unexpected type",
resource,
attribute,
value,
type(value),
)
return value
def __repr__(self):
return (
"AddressResolveResult("
f"original_address=0x{self.original_address:08X}, "
f"file_name={self.file.name!r}, "
f"file_offset=0x{self.file_offset:X})"
)
class AddressResolver(abc.ABC):
@abc.abstractmethod
def resolve(
self, original_address: int, address_offset: int
) -> AddressResolveResult: ...
class MemoryMap:
def __init__(self):
self.direct = RangeMap[AddressResolver]()
self.segments: dict[int, RangeMap[AddressResolver]] = {
segment_num: RangeMap[AddressResolver]() for segment_num in range(1, 16)
}
def copy(self):
"""Returns a copy that is independently mutable
(only the mappings are copied, the underlying AddressResolver s are the same)
"""
other = MemoryMap()
other.direct = self.direct.copy()
other.segments = {
segment_num: segment_range_map.copy()
for segment_num, segment_range_map in self.segments.items()
}
return other
def get_segment_num(address: int):
return (address & 0x0F00_0000) >> 24
@dataclass
class FileDirectAddressResolver(AddressResolver):
direct_file_offset_start: int
target_file: File
def resolve(self, original_address, address_offset):
file_offset = address_offset - self.direct_file_offset_start
return AddressResolveResult(original_address, self.target_file, file_offset)
@dataclass
class FileSegmentAddressResolver(AddressResolver):
target_file: File
def resolve(self, original_address, address_offset):
file_offset = address_offset
return AddressResolveResult(original_address, self.target_file, file_offset)
class MemoryContext:
"""
handles segmented addresses, pointers, external symbols (eg gMtxClear)
maps offsets to data
"""
def __init__(self, dmadata_table_rom_file_name_by_vrom):
self.memory_map = MemoryMap()
self.dmadata_table_rom_file_name_by_vrom = dmadata_table_rom_file_name_by_vrom
def copy(self):
other = MemoryContext(self.dmadata_table_rom_file_name_by_vrom)
other.memory_map = self.memory_map.copy()
return other
def get_dmadata_table_rom_file_name_from_vrom(self, vromStart, vromEnd):
return self.dmadata_table_rom_file_name_by_vrom[(vromStart, vromEnd)]
def _direct_address_to_offset(self, address: int):
segment_num = get_segment_num(address)
if segment_num != 0:
raise ValueError("Address is segmented, not direct", hex(address))
# The 0xF000_0000 bits are ignored. Not 100% correct but simplest
offset = address & 0x00FF_FFFF
return offset
def set_direct_file(self, address: int, target_file: File):
direct_file_offset_start = self._direct_address_to_offset(address)
direct_file_offset_end = direct_file_offset_start + target_file.size
self.memory_map.direct.set(
direct_file_offset_start,
direct_file_offset_end,
FileDirectAddressResolver(direct_file_offset_start, target_file),
)
def set_segment_file(self, segment_num: int, target_file: File):
if not (1 <= segment_num < 16):
raise ValueError(
"Segment number must be between 1 and 15 (inclusive)", segment_num
)
self.memory_map.segments[segment_num].set(
0, 0x0100_0000, FileSegmentAddressResolver(target_file)
)
def resolve_direct(self, address: int):
offset = self._direct_address_to_offset(address)
try:
address_resolver = self.memory_map.direct.get(offset)
except IndexError as e:
raise UnmappedAddressError(
"direct address is not mapped", f"0x{address:08X}"
) from e
return address_resolver.resolve(address, offset)
def resolve_segmented(self, address: int):
segment_num = get_segment_num(address)
if segment_num == 0:
return self.resolve_direct(address)
else:
assert address & 0xF000_0000 == 0
offset = address & 0x00FF_FFFF
try:
address_resolver = self.memory_map.segments[segment_num].get(offset)
except IndexError as e:
raise UnmappedAddressError(
"segment address is not mapped", f"0x{address:08X}"
) from e
return address_resolver.resolve(address, offset)
def report_resource_at_segmented(
self,
reporter: Resource,
address: int,
resource_type: type[ResourceT],
new_resource_pointed_to: Callable[[File, int], ResourceT],
) -> ResourceT:
try:
resolve_result = self.resolve_segmented(address)
except UnmappedAddressError as e:
if BEST_EFFORT:
fake_file = File(f"besteffort_fakefile_{address:08X}", size=0x0100_0000)
fake_resource = new_resource_pointed_to(fake_file, 0)
fake_resource.reporters.add(reporter)
fake_file.add_resource(fake_resource)
if VERBOSE_BEST_EFFORT >= 2 or (
VERBOSE_BEST_EFFORT >= 1
and (address >> 24) not in VERBOSE_BEST_EFFORT_LVL1_IGNORED_SEGS
):
print("BEST_EFFORT: ignored error e=")
rich_pprint(e)
print(" on resource report by reporter=")
rich_pprint(reporter)
print(f" at {address=:#08X}")
print(" and created fake_file=")
rich_pprint(fake_file),
print(" and fake_resource=")
rich_pprint(fake_resource)
fake_file.FAKE_FOR_BEST_EFFORT = True
fake_resource.FAKE_FOR_BEST_EFFORT = True
return fake_resource
raise
try:
resource = resolve_result.get_resource(resource_type)
except NoResourceError:
resource = None
except UnexpectedResourceTypeError:
print("Could not resolve segment address for reporting", resolve_result)
raise
else:
assert resource is not None
if resource is None:
resource = new_resource_pointed_to(
resolve_result.file,
resolve_result.file_offset,
)
resolve_result.file.add_resource(resource)
resource.reporters.add(reporter)
return resource
def mark_resource_buffer_at_segmented(
self,
reporter: Resource,
resource_type: type[Resource],
name: str,
address_start: int,
address_end: int,
):
# Note: this function assumes the whole address_start-address_end range resolves the same way.
# It not being the case would be very weird, but it's not checked here
try:
resolve_result = self.resolve_segmented(address_start)
except UnmappedAddressError as e:
if BEST_EFFORT:
if VERBOSE_BEST_EFFORT >= 2 or (
VERBOSE_BEST_EFFORT >= 1
and (address_start >> 24)
not in VERBOSE_BEST_EFFORT_LVL1_IGNORED_SEGS
):
print("BEST_EFFORT: ignored error e=")
rich_pprint(e)
print(" and skipping marking resource buffer for reporter=")
rich_pprint(reporter)
print(
f" {resource_type=} {address_start=:#08X} {address_end=:#08X}"
)
return
raise
file_start = resolve_result.file_offset
file_end = file_start + address_end - address_start
resolve_result.file.mark_resource_buffer(
reporter, resource_type, name, file_start, file_end
)
def get_attribute_at_segmented(
self, address: int, attribute: Attribute[AttributeValueT]
):
return self.resolve_segmented(address).get_attribute(attribute)
def get_c_reference_at_segmented(self, address: int):
try:
return self.get_attribute_at_segmented(address, Attributes.c_reference)
except UnmappedAddressError as e:
if BEST_EFFORT:
if VERBOSE_BEST_EFFORT >= 2 or (
VERBOSE_BEST_EFFORT >= 1
and (address >> 24) not in VERBOSE_BEST_EFFORT_LVL1_IGNORED_SEGS
):
print("BEST_EFFORT: ignored error e="),
rich_pprint(e)
print(f" and returning raw address=0x{address:08X}")
return f"0x{address:08X}"
raise
def get_c_expression_length_at_segmented(self, address: int):
return self.get_attribute_at_segmented(address, Attributes.c_expression_length)
|