Skip to content

Commit

Permalink
Merge pull request #56 from randovania/revert-script-api
Browse files Browse the repository at this point in the history
revert new script API
  • Loading branch information
henriquegemignani authored Jun 23, 2023
2 parents 730ee09 + 43fc137 commit 60d8c4a
Show file tree
Hide file tree
Showing 8 changed files with 472 additions and 570 deletions.
351 changes: 322 additions & 29 deletions retro_data_structures/formats/mlvl.py

Large diffs are not rendered by default.

363 changes: 36 additions & 327 deletions retro_data_structures/formats/mrea.py

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions retro_data_structures/formats/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from retro_data_structures.common_types import AssetId32, FourCC
from retro_data_structures.formats import dgrp
from retro_data_structures.formats.dgrp import DGRP
from retro_data_structures.formats.script_object import ConstructScriptInstance, ScriptInstance
from retro_data_structures.formats.script_object import ConstructScriptInstance, ScriptInstanceHelper
from retro_data_structures.game_check import Game

ScanImage = Struct(
Expand Down Expand Up @@ -85,12 +85,12 @@ def dependencies_for(self) -> typing.Iterator[Dependency]:
for it in self._internal_dependencies_for():
yield Dependency(it.type, it.id, True)

_scannable_object_info: ScriptInstance | None = None
_scannable_object_info: ScriptInstanceHelper | None = None
@property
def scannable_object_info(self) -> ScriptInstance:
def scannable_object_info(self) -> ScriptInstanceHelper:
assert self.target_game != Game.PRIME
if self._scannable_object_info is None:
self._scannable_object_info = ScriptInstance(self._raw.scannable_object_info, self.target_game,
self._scannable_object_info = ScriptInstanceHelper(self._raw.scannable_object_info, self.target_game,
on_modify=self.rebuild_dependencies)
return self._scannable_object_info

Expand Down
95 changes: 39 additions & 56 deletions retro_data_structures/formats/script_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,13 @@
from retro_data_structures.base_resource import Dependency
from retro_data_structures.common_types import FourCC
from retro_data_structures.construct_extensions.misc import Skip
from retro_data_structures.formats.script_object import (
ConstructScriptInstance,
InstanceId,
InstanceIdRef,
InstanceRef,
ScriptInstance,
resolve_instance_id_ref,
)
from retro_data_structures.formats.script_object import ConstructScriptInstance, InstanceId, ScriptInstanceHelper
from retro_data_structures.game_check import Game
from retro_data_structures.properties import BaseObjectType

if typing.TYPE_CHECKING:
from retro_data_structures.asset_manager import AssetManager
from retro_data_structures.formats.mlvl import Area
from retro_data_structures.formats.mlvl import AreaWrapper

ScriptLayerPrime = Struct(
"magic" / Const("SCLY", FourCC),
Expand All @@ -61,7 +54,7 @@
)


def ConstructScriptLayer(identifier):
def ScriptLayer(identifier):
return Struct(
"magic" / Const(identifier, FourCC),
"unknown" / Int8ub,
Expand All @@ -83,16 +76,12 @@ def new_layer(index: int | None, target_game: Game) -> Container:
})


SCLY = IfThenElse(
game_check.current_game_at_least(game_check.Game.ECHOES),
ConstructScriptLayer("SCLY"),
ScriptLayerPrime
)
SCGN = ConstructScriptLayer("SCGN")
SCLY = IfThenElse(game_check.current_game_at_least(game_check.Game.ECHOES), ScriptLayer("SCLY"), ScriptLayerPrime)
SCGN = ScriptLayer("SCGN")


class ScriptLayer:
_parent_area: Area | None = None
class ScriptLayerHelper:
_parent_area: AreaWrapper | None = None
_index: int
_modified: bool = False

Expand All @@ -106,7 +95,7 @@ def __repr__(self) -> str:
return f"{self.name} ({'Active' if self.active else 'Inactive'})"
return super().__repr__()

def with_parent(self, parent: Area) -> ScriptLayer:
def with_parent(self, parent: AreaWrapper) -> ScriptLayerHelper:
self._parent_area = parent
return self

Expand All @@ -117,68 +106,63 @@ def index(self):
@property
def instances(self):
for instance in self._raw.script_instances:
yield ScriptInstance(instance, self.target_game, on_modify=self.mark_modified)

def has_instance(self, instance: InstanceRef) -> bool:
return self.get_instance(instance, must_exist=False) is not None

def get_instance(self, instance: InstanceRef, *, must_exist: bool = True) -> ScriptInstance | None:
try:
if isinstance(instance, str):
return self._get_instance_by_name(instance)
else:
return self._get_instance_by_ref(instance)
except KeyError:
if must_exist:
raise
return None

def _get_instance_by_ref(self, ref: InstanceIdRef) -> ScriptInstance:
ref = resolve_instance_id_ref(ref)
yield ScriptInstanceHelper(instance, self.target_game, on_modify=self.mark_modified)

def get_instance(self, instance_id: int) -> ScriptInstanceHelper:
for instance in self.instances:
if instance.id_matches(ref):
if instance.id_matches(instance_id):
return instance
raise KeyError(ref)
return None

def _get_instance_by_name(self, name: str) -> ScriptInstance:
def get_instance_by_name(self, name: str, *, raise_if_missing: bool = True) -> ScriptInstanceHelper:
for instance in self.instances:
if instance.name == name:
return instance
raise KeyError(name)
if raise_if_missing:
raise KeyError(name)

def _internal_add_instance(self, instance: ScriptInstance):
if self.has_instance(instance.id):
raise RuntimeError(f"Instance with id {instance.id} already exists")
def _internal_add_instance(self, instance: ScriptInstanceHelper):
if self.get_instance(instance.id) is not None:
raise RuntimeError(f"Instance with id {instance.id} already exists.")

self._modified = True
self._raw.script_instances.append(instance._raw)
return self._get_instance_by_ref(instance.id)
return self.get_instance(instance.id)

def add_instance(self, instance_type: str, name: str | None = None) -> ScriptInstance:
instance = ScriptInstance.new_instance(self.target_game, instance_type, self)
def add_instance(self, instance_type: str, name: str | None = None) -> ScriptInstanceHelper:
instance = ScriptInstanceHelper.new_instance(self.target_game, instance_type, self)
if name is not None:
instance.name = name
return self._internal_add_instance(instance)

def add_instance_with(self, object_properties: BaseObjectType) -> ScriptInstance:
instance = ScriptInstance.new_from_properties(object_properties, self)
def add_instance_with(self, object_properties: BaseObjectType) -> ScriptInstanceHelper:
instance = ScriptInstanceHelper.new_from_properties(object_properties, self)
return self._internal_add_instance(instance)

def add_memory_relay(self, name: str | None = None) -> ScriptInstance:
def add_memory_relay(self, name: str | None = None) -> ScriptInstanceHelper:
relay = self.add_instance("MRLY", name)
savw = self._parent_area._parent_mlvl.savw
savw.raw.memory_relays.append({"instance_id": relay.id})
return relay

def remove_instance(self, instance: InstanceRef):
if isinstance(instance, str):
instance = self._get_instance_by_name(instance)
def add_existing_instance(self, instance: ScriptInstanceHelper) -> ScriptInstanceHelper:
if instance.id.area != self._parent_area.id:
new_id = InstanceId.new(self._index, self._parent_area.id, self._parent_area.next_instance_id)
else:
instance = self._get_instance_by_ref(instance)
new_id = InstanceId.new(self._index, instance.id.area, instance.id.instance)

instance.id = new_id
return self._internal_add_instance(instance)

def remove_instance(self, instance: int | str | ScriptInstanceHelper):
if isinstance(instance, str):
instance = self.get_instance_by_name(instance)
if isinstance(instance, ScriptInstanceHelper):
instance = instance.id

matching_instances = [
i for i in self._raw.script_instances
if i.id == instance.id
if i.id == instance
]

if not matching_instances:
Expand Down Expand Up @@ -227,7 +211,6 @@ def name(self, value: str):
self._parent_area._layer_names[self._index] = value

def new_instance_id(self) -> InstanceId:
self.assert_parent()
return InstanceId.new(self._index, self._parent_area.index, self._parent_area.next_instance_id)

def is_modified(self) -> bool:
Expand Down
54 changes: 25 additions & 29 deletions retro_data_structures/formats/script_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
Int32ub,
PrefixedArray,
Struct,
Union,
)

from retro_data_structures import game_check, properties
Expand All @@ -33,7 +34,7 @@

if TYPE_CHECKING:
from retro_data_structures.asset_manager import AssetManager
from retro_data_structures.formats.script_layer import ScriptLayer
from retro_data_structures.formats.script_layer import ScriptLayerHelper
from retro_data_structures.properties.base_property import BaseObjectType

PropertyType = typing.TypeVar("PropertyType", bound=BaseObjectType)
Expand Down Expand Up @@ -76,7 +77,7 @@ def instance(self) -> int:
return self & 0xffff


@dataclasses.dataclass(frozen=True)
@dataclasses.dataclass()
class Connection:
state: State
message: Message
Expand Down Expand Up @@ -213,7 +214,7 @@ def _try_quick_get_name(data: bytes) -> str | None:
return None


class ScriptInstance:
class ScriptInstanceHelper:
_raw: ScriptInstanceRaw
target_game: Game

Expand All @@ -226,10 +227,10 @@ def __repr__(self):
return f"<ScriptInstance {self.type_name} 0x{self.id:08x}>"

def __eq__(self, other):
return isinstance(other, ScriptInstance) and self._raw == other._raw
return isinstance(other, ScriptInstanceHelper) and self._raw == other._raw

@classmethod
def new_instance(cls, target_game: Game, instance_type: str, layer: ScriptLayer) -> ScriptInstance:
def new_instance(cls, target_game: Game, instance_type: str, layer: ScriptLayerHelper) -> ScriptInstanceHelper:
property_type = properties.get_game_object(target_game, instance_type)

raw = ScriptInstanceRaw(
Expand All @@ -241,7 +242,7 @@ def new_instance(cls, target_game: Game, instance_type: str, layer: ScriptLayer)
return cls(raw, target_game, on_modify=layer.mark_modified)

@classmethod
def new_from_properties(cls, object_properties: BaseObjectType, layer: ScriptLayer) -> ScriptInstance:
def new_from_properties(cls, object_properties: BaseObjectType, layer: ScriptLayerHelper) -> ScriptInstanceHelper:
raw = ScriptInstanceRaw(
type=object_properties.object_type(),
id=layer.new_instance_id(),
Expand All @@ -267,9 +268,11 @@ def id(self, value):
self._raw.id = InstanceId(value)
self.on_modify()

def id_matches(self, other: InstanceIdRef) -> bool:
other = resolve_instance_id_ref(other)
return self.id.area == other.area and self.id.instance == other.instance
def id_matches(self, id: int | InstanceId) -> bool:
if not isinstance(id, InstanceId):
id = InstanceId(id)

return self.id.area == id.area and self.id.instance == id.instance

@property
def name(self) -> str | None:
Expand All @@ -290,7 +293,7 @@ def raw_properties(self) -> bytes:
return self._raw.base_property

def get_properties(self) -> BaseObjectType:
return self.type.from_bytes(self.raw_properties)
return self.type.from_bytes(self._raw.base_property)

def get_properties_as(self, type_cls: type[PropertyType]) -> PropertyType:
props = self.get_properties()
Expand All @@ -308,6 +311,12 @@ def set_properties(self, data: BaseObjectType):
self._raw.base_property = data.to_bytes()
self.on_modify()

def get_property(self, chain: Iterator[str]):
prop = self.get_properties()
for name in chain:
prop = getattr(prop, name)
return prop

@contextlib.contextmanager
def edit_properties(self, type_cls: type[PropertyType]):
props = self.get_properties_as(type_cls)
Expand All @@ -323,37 +332,24 @@ def connections(self, value: typing.Iterable[Connection]):
self._raw.connections = tuple(value)
self.on_modify()

def add_connection(self, state: str | State, message: str | Message, target: InstanceIdRef):
target = resolve_instance_id_ref(target)

def add_connection(self, state: str | State, message: str | Message, target: ScriptInstanceHelper):
correct_state = enum_helper.STATE_PER_GAME[self.target_game]
correct_message = enum_helper.MESSAGE_PER_GAME[self.target_game]

self.connections = self.connections + (Connection(
state=_resolve_to_enum(correct_state, state),
message=_resolve_to_enum(correct_message, message),
target=target
target=target.id
),)

def remove_connection(self, connection: Connection):
self.connections = [c for c in self.connections if c is not connection]

def remove_connections(self, target: InstanceIdRef):
target = resolve_instance_id_ref(target)
def remove_connections(self, target: Union[int, ScriptInstanceHelper]):
if isinstance(target, ScriptInstanceHelper):
target = target.id

self.connections = [c for c in self.connections if c.target != target]

def mlvl_dependencies_for(self, asset_manager: AssetManager) -> Iterator[Dependency]:
yield from self.get_properties().dependencies_for(asset_manager)


InstanceIdRef = int | InstanceId | ScriptInstance
InstanceRef = InstanceIdRef | str

def resolve_instance_id_ref(inst: InstanceIdRef) -> InstanceId:
if isinstance(inst, InstanceId):
return inst
if isinstance(inst, ScriptInstance):
return inst.id
if isinstance(inst, int):
return InstanceId(inst)
raise TypeError(f"Invalid type: Expected InstanceIdRef, got {type(inst)}")
23 changes: 18 additions & 5 deletions test/formats/test_mrea.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import pytest

from retro_data_structures.base_resource import AssetId
from retro_data_structures.formats import Mlvl
from retro_data_structures.formats.mrea import Mrea
from retro_data_structures.formats.script_object import ScriptInstance
from retro_data_structures.formats.script_object import ScriptInstanceHelper


@pytest.mark.xfail
Expand All @@ -24,13 +25,25 @@ def test_compare_p2(prime2_asset_manager, mrea_asset_id: AssetId):
resource = prime2_asset_manager.get_raw_asset(mrea_asset_id)

decoded = Mrea.parse(resource.data, target_game=prime2_asset_manager.target_game)
for inst in decoded._all_non_scgn_instances():
assert isinstance(inst, ScriptInstance)
for instance in decoded.all_instances:
assert isinstance(instance, ScriptInstanceHelper)

encoded = decoded.build()

decoded2 = Mrea.parse(encoded, target_game=prime2_asset_manager.target_game)
for inst in decoded2._all_non_scgn_instances():
assert isinstance(inst, ScriptInstance)
for instance in decoded2.all_instances:
assert isinstance(instance, ScriptInstanceHelper)

assert test_lib.purge_hidden(decoded2.raw) == test_lib.purge_hidden(decoded.raw)


def test_add_instance(prime2_asset_manager):
from retro_data_structures.enums import echoes
from retro_data_structures.properties.echoes.objects.SpecialFunction import SpecialFunction

mlvl = prime2_asset_manager.get_parsed_asset(0x42b935e4, type_hint=Mlvl)
area = mlvl.get_area(0x5DFA984F)
area.get_layer("Default").add_instance_with(SpecialFunction(
function=echoes.Function.Darkworld,
))
assert area.mrea.build() is not None
Loading

0 comments on commit 60d8c4a

Please sign in to comment.