Skip to content

Commit

Permalink
Merge pull request #57 from randovania/feature/script-api-pt2
Browse files Browse the repository at this point in the history
new script API (again)
  • Loading branch information
duncathan authored Jun 23, 2023
2 parents 60d8c4a + ff1260a commit a5b187f
Show file tree
Hide file tree
Showing 8 changed files with 550 additions and 457 deletions.
314 changes: 5 additions & 309 deletions retro_data_structures/formats/mlvl.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
"""
from __future__ import annotations

import itertools
import typing
from collections.abc import Iterator
from itertools import count

import construct
from construct import (
Expand Down Expand Up @@ -38,17 +36,14 @@
from retro_data_structures.construct_extensions.misc import PrefixedArrayWithExtra
from retro_data_structures.exceptions import UnknownAssetId
from retro_data_structures.formats import Mapw
from retro_data_structures.formats.cmdl import dependencies_for_material_set
from retro_data_structures.formats.guid import GUID
from retro_data_structures.formats.mrea import Mrea
from retro_data_structures.formats.mrea import Area
from retro_data_structures.formats.savw import Savw
from retro_data_structures.formats.script_layer import ScriptLayerHelper, new_layer
from retro_data_structures.formats.script_object import InstanceId, ScriptInstanceHelper
from retro_data_structures.formats.strg import Strg
from retro_data_structures.game_check import Game

if typing.TYPE_CHECKING:
from retro_data_structures.asset_manager import AssetManager
pass

MLVLConnectingDock = Struct(
area_index=Int32ub,
Expand Down Expand Up @@ -263,305 +258,6 @@ def create(version: int, asset_id):
)


_hardcoded_dependencies: dict[int, dict[str, list[Dependency]]] = {
0xD7C3B839: {
# Sanctum
"Default": [Dependency("TXTR", 0xd5b9e5d1)],
"Emperor Ing Stage 1": [Dependency("TXTR", 0x52c7d438)],
"Emperor Ing Stage 3": [Dependency("TXTR", 0xd5b9e5d1)],
"Emperor Ing Stage 1 Intro Cine": [Dependency("TXTR", 0x52c7d438)],
"Emperor Ing Stage 3 Death Cine": [Dependency("TXTR", 0xd5b9e5d1)],
},
0xA92F00B3: {
# Hive Temple
"CliffsideBoss": [
Dependency("TXTR", 0x24149e16),
Dependency("TXTR", 0xbdb8a88a),
Dependency("FSM2", 0x3d31822b),
]
},
0xC0113CE8: {
# Dynamo Works
"3rd Pass": [Dependency("RULE", 0x393ca543)]
},
0x5571E89E: {
# Hall of Combat Mastery
"2nd Pass Enemies": [Dependency("RULE", 0x393ca543)]
},
0x7B94B06B: {
# Hive Portal Chamber
"1st Pass": [Dependency("RULE", 0x393ca543)],
"2nd Pass": [Dependency("RULE", 0x393ca543)]
},
0xF8DBC03D: {
# Hive Reactor
"2nd Pass": [Dependency("RULE", 0x393ca543)]
},
0xB666B655: {
# Reactor Access
"2nd Pass": [Dependency("RULE", 0x393ca543)]
},
0xE79AAFAE: {
# Transport A Access
"2nd Pass": [Dependency("RULE", 0x393ca543)]
},
0xFEB7BD27: {
# Transport B Access
"Default": [Dependency("RULE", 0x393ca543)]
},
0x89D246FD: {
# Portal Access
"Default": [Dependency("RULE", 0x393ca543)]
},
0x0253782D: {
# Dark Forgotten Bridge
"Default": [Dependency("RULE", 0x393ca543)]
},
0x09DECF21: {
# Forgotten Bridge
"Default": [Dependency("RULE", 0x393ca543)]
},
0x629790F4: {
# Sacrificial Chamber
"1st Pass": [Dependency("RULE", 0x393ca543)]
},
0xBBE4B3AE: {
# Dungeon
"Default": [Dependency("TXTR", 0xe252e7f6)]
},
0x2BCD44A7: {
# Portal Terminal
"Default": [Dependency("TXTR", 0xb6fa5023)]
},
0xC68B5B51: {
# Transport to Sanctuary Fortress
"!!non_layer!!": [Dependency("TXTR", 0x75a219a8)]
},
0x625A2692: {
# Temple Transport Access
"!!non_layer!!": [Dependency("TXTR", 0x581c56ea)]
},
0x96F4CA1E: {
# Minigyro Chamber
"Default": [Dependency("TXTR", 0xac080dfb)]
},
0x5BBF334F: {
# Staging Area
"!!non_layer!!": [Dependency("TXTR", 0x738feb19)]
}
}


class AreaWrapper:
_flags: Container
_layer_names: ListContainer
_index: int

_mrea: Mrea = None
_strg: Strg = None

# FIXME: since the whole Mlvl is now being passed, this function can have the other arguments removed
def __init__(self, raw: Container, asset_manager: AssetManager, flags: Container, names: Container,
index: int, parent_mlvl: Mlvl):
self._raw = raw
self.asset_manager = asset_manager
self._flags = flags
self._layer_names = names
self._index = index
self._parent_mlvl = parent_mlvl

@property
def id(self) -> int:
return self._raw.internal_area_id

@property
def index(self) -> int:
return self._index

@property
def name(self) -> str:
try:
return self.strg.strings[0]
except UnknownAssetId:
return "!!" + self.internal_name

@name.setter
def name(self, value):
self.strg.set_string(0, value)

@property
def internal_name(self) -> str:
return self._raw.get("internal_area_name", "Unknown")

@property
def strg(self) -> Strg:
if self._strg is None:
self._strg = self.asset_manager.get_file(self._raw.area_name_id, type_hint=Strg)
return self._strg

@property
def mrea(self) -> Mrea:
if self._mrea is None:
self._mrea = self.asset_manager.get_file(self.mrea_asset_id, type_hint=Mrea)
return self._mrea

@property
def mrea_asset_id(self) -> int:
return self._raw.area_mrea_id

@property
def layers(self) -> Iterator[ScriptLayerHelper]:
for layer in self.mrea.script_layers:
yield layer.with_parent(self)

@property
def generated_objects_layer(self) -> ScriptLayerHelper:
return self.mrea.generated_objects_layer

def get_layer(self, name: str) -> ScriptLayerHelper:
return next(layer for layer in self.layers if layer.name == name)

def add_layer(self, name: str, active: bool = True) -> ScriptLayerHelper:
index = len(self._layer_names)
self._layer_names.append(name)
self._flags.append(active)
raw = new_layer(index, self.asset_manager.target_game)
self.mrea._raw.sections.script_layer_section.append(raw)
return self.get_layer(name)

@property
def next_instance_id(self) -> int:
ids = [instance.id.instance for layer in self.layers for instance in layer.instances]
return next(i for i in count() if i not in ids)

def get_instance(self, instance_id: int | InstanceId) -> ScriptInstanceHelper | None:
if not isinstance(instance_id, InstanceId):
instance_id = InstanceId(instance_id)

for layer in self.layers:
if instance_id.layer == layer.index:
return layer.get_instance(instance_id)

return None

def get_instance_by_name(self, name: str) -> ScriptInstanceHelper:
return self.mrea.get_instance_by_name(name)

def _raw_connect_to(self, source_dock_number: int, target_area: AreaWrapper, target_dock_number: int):
source_dock = self._raw.docks[source_dock_number]
assert len(source_dock.connecting_dock) == 1, "Only docks with one connection supported"
source_dock.connecting_dock[0].area_index = target_area._index
source_dock.connecting_dock[0].dock_index = target_dock_number

attached_area_index = []
for docks in self._raw.docks:
for c in docks.connecting_dock:
if c.area_index not in attached_area_index:
attached_area_index.append(c.area_index)
self._raw.attached_area_index = construct.ListContainer(attached_area_index)

def connect_dock_to(self, source_dock_number: int, target_area: AreaWrapper, target_dock_number: int):
self._raw_connect_to(source_dock_number, target_area, target_dock_number)
target_area._raw_connect_to(target_dock_number, self, source_dock_number)

def build_non_layer_dependencies(self) -> typing.Iterator[Dependency]:
if self.asset_manager.target_game <= Game.ECHOES:
geometry_section = self.mrea.get_raw_section("geometry_section")
if geometry_section:
for asset_id in PrefixedArray(Int32ub, AssetId32).parse(geometry_section[0]):
yield from self.asset_manager.get_dependencies_for_asset(asset_id)
else:
geometry = self.mrea.get_geometry()
if geometry is not None:
yield from dependencies_for_material_set(geometry[0].materials, self.asset_manager)

valid_asset = self.asset_manager.target_game.is_valid_asset_id
if valid_asset(portal_area := self.mrea.get_portal_area()):
yield Dependency("PTLA", portal_area)
if valid_asset(static_geometry_map := self.mrea.get_static_geometry_map()):
yield Dependency("EGMC", static_geometry_map)
if valid_asset(path := self.mrea.get_path()):
yield Dependency("PATH", path)

def build_scgn_dependencies(self, layer_deps: list[list[Dependency]], only_modified: bool = False):
layer_deps = list(layer_deps)

layers = list(self.layers)
for instance in self.generated_objects_layer.instances:
inst_layer = instance.id.layer
if not only_modified or layers[inst_layer].is_modified:
layer_deps[inst_layer].extend(instance.mlvl_dependencies_for(self.asset_manager))

return [list(dict.fromkeys(deps)) for deps in layer_deps]

def build_mlvl_dependencies(self, only_modified: bool = False):
layer_deps = [
list(
layer.build_mlvl_dependencies(self.asset_manager)
if (not only_modified) or layer.is_modified() else
layer.dependencies
) for layer in self.layers
]

if only_modified:
# assume we never modify these
layer_deps.append(list(self.non_layer_dependencies))
else:
non_layer_deps = list(self.build_non_layer_dependencies())
if "!!non_layer!!" in _hardcoded_dependencies.get(self.mrea_asset_id, {}):
non_layer_deps.extend(_hardcoded_dependencies[self.mrea_asset_id]["!!non_layer!!"])
layer_deps.append(non_layer_deps)


layer_deps = self.build_scgn_dependencies(layer_deps, only_modified)

if self.mrea_asset_id in _hardcoded_dependencies:
for layer_name, missing in _hardcoded_dependencies[self.mrea_asset_id].items():
if layer_name == "!!non_layer!!":
continue

layer = self.get_layer(layer_name)
if only_modified and not layer.is_modified:
continue

layer_deps[layer.index].extend(missing)

layer_deps = [
[dep for dep in layer if not dep.exclude_for_mlvl]
for layer in layer_deps
]

offset = 0
offsets = []
for layer_dep in layer_deps:
offsets.append(offset)
offset += len(layer_dep)

fancy_deps: list[Dependency] = list(itertools.chain(*layer_deps))
deps = [Container(asset_type=dep.type, asset_id=dep.id) for dep in fancy_deps]
self._raw.dependencies.dependencies = deps
self._raw.dependencies.offsets = offsets

@property
def layer_dependencies(self) -> dict[str, list[Dependency]]:
return {
layer.name: list(layer.dependencies)
for layer in self.layers
}

@property
def non_layer_dependencies(self) -> Iterator[Dependency]:
deps = self._raw.dependencies
global_deps = deps.dependencies[deps.offsets[len(self._layer_names)]:]
yield from [Dependency(dep.asset_type, dep.asset_id) for dep in global_deps]

@property
def dependencies(self) -> dict[str, list[Dependency]]:
deps = self.layer_dependencies
deps["!!non_layer!!"] = list(self.non_layer_dependencies)
return deps


class Mlvl(BaseResource):
_mapw: Mapw = None
_savw: Savw = None
Expand All @@ -586,14 +282,14 @@ def __repr__(self) -> str:
return super().__repr__()

@property
def areas(self) -> Iterator[AreaWrapper]:
def areas(self) -> Iterator[Area]:
offsets = self._raw.area_layer_name_offset
names = self._raw.layer_names
for i, area in enumerate(self._raw.areas):
area_layer_names = names[offsets[i]:] if i == len(self._raw.areas) - 1 else names[offsets[i]:offsets[i+1]]
yield AreaWrapper(area, self.asset_manager, self._raw.area_layer_flags[i], area_layer_names, i, self)
yield Area(area, self.asset_manager, self._raw.area_layer_flags[i], area_layer_names, i, self)

def get_area(self, asset_id: NameOrAssetId) -> AreaWrapper:
def get_area(self, asset_id: NameOrAssetId) -> Area:
return next(area for area in self.areas if area.mrea_asset_id == self.asset_manager._resolve_asset_id(asset_id))

_name_strg_cached: Strg = None
Expand Down
Loading

0 comments on commit a5b187f

Please sign in to comment.