Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/szio/gta5/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
MAP_GRASS_INSTANCES_DTYPE,
MAP_LOD_LIGHT_DTYPE,
AssetMapData,
AssetMapParentTxds,
MapBlockDescription,
MapBoxOccluder,
MapCarGenerator,
Expand Down
1 change: 1 addition & 0 deletions src/szio/gta5/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class AssetType(Enum):
CLOTH_DICTIONARY = auto()
MAP_TYPES = auto()
MAP_DATA = auto()
MAP_PARENT_TXDS = auto()
TEXTURE_DICTIONARY = auto()


Expand Down
2 changes: 2 additions & 0 deletions src/szio/gta5/cwxml/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@
)
from .map import (
load_map_data_from_cw,
load_map_parent_txds_from_cw,
save_map_data_to_cw,
save_map_parent_txds_to_cw,
)
22 changes: 22 additions & 0 deletions src/szio/gta5/cwxml/adapters/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ... import jenkhash
from ...entities import Entity, MapEntity
from ...maps import (
AssetMapParentTxds,
MAP_DISTANT_LOD_LIGHT_DTYPE,
MAP_GRASS_INSTANCES_DTYPE,
MAP_LOD_LIGHT_DTYPE,
Expand Down Expand Up @@ -405,3 +406,24 @@ def save_map_data_to_cw(asset: AssetMapData) -> cw.CMapData:
if asset.description is not None:
_save_description(asset.description, t.block)
return t


def load_map_parent_txds_from_cw(t: cw.CMapParentTxds) -> AssetMapParentTxds:
parents = {}
for relationship in t.txd_relationships:
if relationship.parent and relationship.child:
# A texture dictionary can only inherit from a single parent, keep the first one
parents.setdefault(relationship.child, relationship.parent)

return AssetMapParentTxds(parents=parents)


def save_map_parent_txds_to_cw(asset: AssetMapParentTxds) -> cw.CMapParentTxds:
t = cw.CMapParentTxds()
for child, parent in asset.parents.items():
relationship = cw.TxdRelationship()
relationship.parent = parent
relationship.child = child
t.txd_relationships.append(relationship)

return t
25 changes: 24 additions & 1 deletion src/szio/gta5/cwxml/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ..cloths import AssetClothDictionary
from ..drawables import AssetDrawable, AssetDrawableDictionary
from ..fragments import AssetFragment
from ..maps import AssetMapData
from ..maps import AssetMapData, AssetMapParentTxds
from ..textures import AssetTextureDictionary
from . import bound as cwbnd
from . import cloth as cwcloth
Expand All @@ -32,6 +32,8 @@
save_drawable_to_cw,
save_fragment_to_cw,
save_map_data_to_cw,
load_map_parent_txds_from_cw,
save_map_parent_txds_to_cw,
save_map_types_to_cw,
save_txd_to_cw,
)
Expand All @@ -53,7 +55,21 @@ class CWProvider(ABC):
".ytd": "TextureDictionary",
}

#: gtxd files are plain XML but do not follow the `<name>.<ext>.xml` convention of the other assets
GTXD_EXTENSIONS = (".meta", ".ymt.rbf.xml")

def _supports_gtxd_file(self, path: ProviderPath) -> bool:
name = path.name.lower()
if not any(name.endswith(ext) for ext in CWProvider.GTXD_EXTENSIONS) or not path.is_file():
return False

with import_source(path) as src:
return get_xml_root_tag(src) == "CMapParentTxds"

def supports_file(self, path: ProviderPath) -> bool:
if self._supports_gtxd_file(path):
return True

suffixes = path.suffixes
if (
len(suffixes) >= 2
Expand All @@ -69,6 +85,10 @@ def supports_file(self, path: ProviderPath) -> bool:
return False

def load_file(self, path: ProviderPath) -> Asset:
if self._supports_gtxd_file(path):
with import_source(path) as src:
return load_map_parent_txds_from_cw(cwmap.CMapParentTxds.from_xml_file(src))

suffixes = path.suffixes
with import_source(path) as src:
match suffixes[-2].lower():
Expand Down Expand Up @@ -118,6 +138,9 @@ def save_asset(self, asset: Asset, directory: Path, name: str, tool_metadata: tu
elif isinstance(asset, AssetTextureDictionary):
path = directory / f"{name}.ytd.xml"
save_txd_to_cw(asset).write_xml(path)
elif isinstance(asset, AssetMapParentTxds):
path = directory / f"{name}.meta"
save_map_parent_txds_to_cw(asset).write_xml(path)
else:
raise ValueError(f"Unsupported asset '{asset}' (name: '{name}', directory: '{str(directory)}')")

Expand Down
31 changes: 31 additions & 0 deletions src/szio/gta5/cwxml/ymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -774,3 +774,34 @@ def __init__(self):
self.lod_lights_soa = LODLightsSOA()
self.distant_lod_lights_soa = DistantLODLightsSOA()
self.block = Block()


class TxdRelationship(ElementTree):
tag_name = "item"

def __init__(self):
super().__init__()
self.parent = TextPropertyRequired("parent")
self.child = TextPropertyRequired("child")


class TxdRelationshipsList(ListPropertyRequired):
list_type = TxdRelationship
tag_name = "txdRelationships"

@classmethod
def from_xml(cls, element: ET.Element):
# `gtxd.ymt` uses `item` and `gtxd.meta` uses `Item`, accept both
new = cls(element.tag)
new.value.extend(TxdRelationship.from_xml(child) for child in element if child.tag.lower() == "item")
return new


class CMapParentTxds(ElementTree):
"""Parent-child texture dictionary relationships, as stored in `gtxd.meta` and `gtxd.ymt`."""

tag_name = "CMapParentTxds"

def __init__(self):
super().__init__()
self.txd_relationships = TxdRelationshipsList()
15 changes: 15 additions & 0 deletions src/szio/gta5/maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,18 @@ class AssetMapData:
lod_lights: MapLodLights | None = None
distant_lod_lights: MapDistantLodLights | None = None
description: MapBlockDescription | None = None


@dataclass(slots=True)
class AssetMapParentTxds:
"""Parent-child texture dictionary relationships, as stored in `gtxd.meta` and `gtxd.ymt`.

A texture dictionary inherits the textures of its parent, and a parent may itself be the child of another
texture dictionary, so the relationships can form a hierarchy of any depth.
"""

ASSET_GAME: AssetGame = AssetGame.GTA5
ASSET_TYPE: AssetType = AssetType.MAP_PARENT_TXDS

#: Maps each child texture dictionary to the name of its parent.
parents: dict[str, str] = field(default_factory=dict)