mirror of
https://github.com/zeldaret/oot.git
synced 2025-05-11 19:43:44 +00:00
318 lines
11 KiB
Python
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
|