1
0
Fork 0
mirror of https://github.com/zeldaret/oot.git synced 2025-05-11 19:43:44 +00:00
oot/tools/assets/descriptor/base.py
2025-02-18 10:07:20 +01:00

318 lines
11 KiB
Python

# SPDX-FileCopyrightText: © 2025 ZeldaRET
# SPDX-License-Identifier: CC0-1.0
import abc
import dataclasses
from functools import cache
from pathlib import Path
from typing import Callable, Optional
from xml.etree import ElementTree
from tools import version_config
class BackingMemory(abc.ABC):
pass
@dataclasses.dataclass
class BaseromFileBackingMemory(BackingMemory):
name: str
range: Optional[tuple[int, int]]
"""If set, consider file_data[range[0]:range[1]] instead of the full file"""
@dataclasses.dataclass
class NoBackingMemory(BackingMemory):
pass
# eq=False so this uses id-based equality and hashing
# Subclasses must also be made to use id-based equality and hashing
@dataclasses.dataclass(eq=False)
class ResourceDesc(abc.ABC):
"""A resource is a data unit.
For example, a symbol's data such as a DList or a texture."""
symbol_name: str
offset: int
"""How many bytes into the backing memory the resource is located at"""
collection: "ResourcesDescCollection" = dataclasses.field(repr=False)
origin: object
"""opaque object with data about where this resource comes from (for debugging)"""
hack_modes: set[str] = dataclasses.field(init=False, default_factory=set)
class StartAddress(abc.ABC):
pass
@dataclasses.dataclass
class VRAMStartAddress(StartAddress):
vram: int
@dataclasses.dataclass
class SegmentStartAddress(StartAddress):
segment: int
@dataclasses.dataclass(eq=False)
class ResourcesDescCollection:
"""A collection is a list of resources backed by the same memory."""
out_path: Path
backing_memory: BackingMemory
start_address: Optional[StartAddress]
resources: list[ResourceDesc]
last_modified_time: float
depends: list["ResourcesDescCollection"]
@dataclasses.dataclass(eq=False)
class ResourcesDescCollectionsPool:
"""A pool contains a minimal set of interconnected collections.
For example, gkeep and all files using gkeep,
or more simply a single collection with no connection."""
collections: list[ResourcesDescCollection]
ResourceHandlerPass2Callback = Callable[[ResourcesDescCollectionsPool], None]
@dataclasses.dataclass
class ResourceHandlerNeedsPass2Exception(Exception):
resource: ResourceDesc
pass2_callback: ResourceHandlerPass2Callback
# eq=False so this uses id-based equality and hashing
@dataclasses.dataclass(eq=False)
class AssetConfigPiece:
ac: version_config.AssetConfig
last_modified_time: float = None
etree: ElementTree.ElementTree = None
depends: list["AssetConfigPiece"] = dataclasses.field(default_factory=list)
"""The AssetConfigPiece s this instance depends on"""
collections: list[ResourcesDescCollection] = dataclasses.field(default_factory=list)
def get_resources_desc(vc: version_config.VersionConfig):
# Wrap AssetConfig objects in AssetConfigPiece for hashability and to collect data
acps = [AssetConfigPiece(ac) for ac in vc.assets]
# Parse xmls
for acp in acps:
acp.last_modified_time = acp.ac.xml_path.stat().st_mtime
try:
with acp.ac.xml_path.open(encoding="UTF-8") as f:
etree = ElementTree.parse(f)
acp.etree = etree
except Exception as e:
raise Exception(f"Error when parsing XML for {acp}") from e
# Resolve pools
acp_by_name = {acp.ac.name: acp for acp in acps}
pools = {acp: {acp} for acp in acps}
for acp in acps:
try:
rootelem = acp.etree.getroot()
assert rootelem.tag == "Root", rootelem.tag
for fileelem in rootelem:
assert fileelem.tag in {"ExternalFile", "File"}, fileelem.tag
if fileelem.tag == "ExternalFile":
externalfile_name = str(
Path(fileelem.attrib["OutPath"]).relative_to("assets")
)
assert externalfile_name in acp_by_name, externalfile_name
externalfile_acp = acp_by_name[externalfile_name]
acp.depends.append(externalfile_acp)
acp_pool = pools[acp]
externalfile_acp_pool = pools[externalfile_acp]
merged_pool = acp_pool | externalfile_acp_pool
for merged_pool_acp in merged_pool:
pools[merged_pool_acp] = merged_pool
except Exception as e:
raise Exception(f"Error while resolving pools with {acp}") from e
# List unique pools
pools_unique: list[set[AssetConfigPiece]] = []
while pools:
pool = next(iter(pools.values()))
pools_unique.append(pool)
for acp in pool:
del pools[acp]
# Build resources for all pools
pools: list[ResourcesDescCollectionsPool] = []
for pool in pools_unique:
try:
all_needs_pass2_exceptions: list[ResourceHandlerNeedsPass2Exception] = []
rescolls: list[ResourcesDescCollection] = []
# Pass 1: create Resource objects
for acp in pool:
try:
rootelem = acp.etree.getroot()
for fileelem in rootelem:
if fileelem.tag == "File":
rc, needs_pass2_exceptions = (
_get_resources_fileelem_to_resourcescollection_pass1(
vc, pool, acp, fileelem
)
)
acp.collections.append(rc)
rescolls.append(rc)
all_needs_pass2_exceptions.extend(needs_pass2_exceptions)
except Exception as e:
raise Exception(f"Error with {acp}") from e
rcpool = ResourcesDescCollectionsPool(rescolls)
#
for acp in pool:
for acp_coll in acp.collections:
acp_coll.depends.extend(
(_coll for _coll in acp.collections if _coll != acp_coll)
)
for acp_dep in acp.depends:
acp_coll.depends.extend(acp_dep.collections)
# Pass 2: execute callbacks
for needs_pass2_exc in all_needs_pass2_exceptions:
try:
needs_pass2_exc.pass2_callback(rcpool)
except Exception as e:
raise Exception(
f"Error with pass 2 callback for {needs_pass2_exc.resource}"
) from e
pools.append(rcpool)
except Exception as e:
raise Exception(f"Error with pool {pool}") from e
return pools
def _get_resources_fileelem_to_resourcescollection_pass1(
vc: version_config.VersionConfig,
pool: list[AssetConfigPiece],
acp: AssetConfigPiece,
fileelem: ElementTree.Element,
):
# Determine backing_memory
if acp.ac.start_offset is None:
assert acp.ac.end_offset is None
baserom_file_range = None
else:
assert acp.ac.end_offset is not None
baserom_file_range = (acp.ac.start_offset, acp.ac.end_offset)
backing_memory = BaseromFileBackingMemory(
name=fileelem.attrib["Name"],
range=baserom_file_range,
)
# Determine start_address
if any(
acp.ac.name.startswith(_prefix) for _prefix in ("code/", "n64dd/", "overlays/")
):
# File start address is vram
assert "Segment" not in fileelem.attrib
assert acp.ac.start_offset is not None and acp.ac.end_offset is not None, (
"Unsupported combination: "
f"start/end offset not in config for vram asset {acp.ac.name}"
)
if acp.ac.name.startswith("overlays/"):
overlay_name = acp.ac.name.split("/")[1]
start_address = VRAMStartAddress(
vc.dmadata_segments[overlay_name].vram + acp.ac.start_offset
)
else:
file_name = acp.ac.name.split("/")[0] # "code" or "n64dd"
start_address = VRAMStartAddress(
vc.dmadata_segments[file_name].vram + acp.ac.start_offset
)
elif "Segment" in fileelem.attrib:
# File start address is a segmented address
assert acp.ac.start_offset is None and acp.ac.end_offset is None, (
"Unsupported combination: "
"start/end offset in config and file starts at a segmented address"
)
start_address = SegmentStartAddress(int(fileelem.attrib["Segment"]))
else:
# File does not have a start address
start_address = None
# resources
resources: list[ResourceDesc] = []
collection = ResourcesDescCollection(
Path(acp.ac.name),
backing_memory,
start_address,
resources,
acp.last_modified_time,
[],
)
needs_pass2_exceptions: list[ResourceHandlerNeedsPass2Exception] = []
for reselem in fileelem:
try:
symbol_name = reselem.attrib["Name"]
offset = int(reselem.attrib["Offset"], 16)
res_handler = _get_resource_handler(reselem.tag)
try:
res = res_handler(symbol_name, offset, collection, reselem)
except ResourceHandlerNeedsPass2Exception as needs_pass2_exc:
res = needs_pass2_exc.resource
needs_pass2_exceptions.append(needs_pass2_exc)
assert isinstance(res, ResourceDesc)
resources.append(res)
except Exception as e:
raise Exception(
"Error with resource element:\n"
+ ElementTree.tostring(reselem, encoding="unicode")
) from e
return collection, needs_pass2_exceptions
ResourceHandler = Callable[
[str, int, ResourcesDescCollection, ElementTree.Element],
ResourceDesc,
]
@cache
def _get_resource_handler(tag: str) -> ResourceHandler:
from . import n64resources
from . import z64resources
resource_handlers = {
"DList": n64resources.handler_DList,
"Blob": n64resources.handler_Blob,
"Mtx": n64resources.handler_Mtx,
"Array": n64resources.handler_Array,
"Texture": n64resources.handler_Texture,
"Collision": z64resources.handler_Collision,
"Animation": z64resources.handler_Animation,
"PlayerAnimation": z64resources.handler_PlayerAnimation,
"LegacyAnimation": z64resources.handler_LegacyAnimation,
"Cutscene": z64resources.handler_Cutscene,
"Scene": z64resources.handler_Scene,
"Room": z64resources.handler_Room,
"PlayerAnimationData": z64resources.handler_PlayerAnimationData,
"Path": z64resources.handler_PathList,
"Skeleton": z64resources.handler_Skeleton,
"Limb": z64resources.handler_Limb,
"CurveAnimation": z64resources.handler_CurveAnimation,
"LimbTable": z64resources.handler_LimbTable,
}
rh = resource_handlers.get(tag)
if rh is None:
raise Exception(f"Unknown resource tag {tag}")
else:
return rh