1
0
Fork 0
mirror of https://github.com/zeldaret/oot.git synced 2025-05-11 03:23:46 +00:00
oot/tools/assets/extract/extase/memorymap.py
2025-02-18 21:31:51 +01:00

428 lines
15 KiB
Python

# 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)