diff --git a/src/mercury_engine_data_structures/formats/bmslink.py b/src/mercury_engine_data_structures/formats/bmslink.py index 2b06c610..ff35df58 100644 --- a/src/mercury_engine_data_structures/formats/bmslink.py +++ b/src/mercury_engine_data_structures/formats/bmslink.py @@ -7,6 +7,7 @@ from construct.core import ( Byte, Const, + Flag, Int32ul, LazyBound, PrefixedArray, @@ -20,16 +21,16 @@ from mercury_engine_data_structures.game_check import Game UnkStruct = Struct( - unk1=StrId, - unk2=Byte, - unk3=Float, - unk4=StrId, - unk5=StrId, - unk6=Float, - unk7=Byte, - unk8=construct.core.If( - construct.this.unk5 != "Portal", - StrId, + dir=StrId, # 16 directions (i.e. RIGHT, RIGHT_UP_27, RIGHT_UP_45, RIGHT_UP_63, UP, ...) + unk2=Flag, + distance=Float, + func=StrId, # empty string, Min or Equals + edge_type=StrId, # Portal, Forbidden, Solid, Stitched or Any + unk6=Float, # 0 or 51? + unk7=Flag, + other_dir=construct.core.If( + construct.this.edge_type != "Portal", + StrId, # right or left, usually opposite of dir ), children=PrefixedArray(Int32ul, LazyBound(lambda: UnkStruct)), ) @@ -37,19 +38,19 @@ Item = Struct( name=StrId, type=StrId, - unk1=StrId, - unk2=Float, - unk3=StrId, - unk4=Float, - unk5=StrId, - unk6=Byte, - unk7=Byte, - unk8=StrId, - unk9=Byte, - unk10=Byte, - unk11=Byte, - unk12=Float, - unk13=UnkStruct, + unk1=Const("", StrId), # possibly empty string? + unk2=Float, # 0-300 + unk3=StrId, # empty or 'tunnel' + unk4=Float, # 0-300 + unk5=StrId, # empty or 'tunnel' + unk6=Flag, + unk7=Flag, + unk8=StrId, # name of action? + unk9=Flag, + unk10=Flag, + unk11=Flag, + unk12=Float, # 0 or 50 + actions=UnkStruct, ) LocationStruct = Struct(name=StrId, items=PrefixedArray(Int32ul, Item)) diff --git a/src/mercury_engine_data_structures/formats/bmsnav.py b/src/mercury_engine_data_structures/formats/bmsnav.py index f23fa15a..6b9f8f75 100644 --- a/src/mercury_engine_data_structures/formats/bmsnav.py +++ b/src/mercury_engine_data_structures/formats/bmsnav.py @@ -1,178 +1,221 @@ from __future__ import annotations -from construct.core import Array, Byte, Const, Construct, Flag, Hex, Int32ul, PrefixedArray, Struct, Terminated +from construct.core import ( + BitsInteger, + BitStruct, + Byte, + ByteSwapped, + Const, + Construct, + Flag, + Int32ul, + PrefixedArray, + Struct, + Terminated, +) from mercury_engine_data_structures.base_resource import BaseResource -from mercury_engine_data_structures.common_types import CVector2D, CVector3D, Float, StrId, VersionAdapter, make_dict +from mercury_engine_data_structures.common_types import ( + CVector2D, + CVector3D, + Float, + StrId, + VersionAdapter, + make_dict, + make_vector, +) from mercury_engine_data_structures.game_check import Game ### A ton of barely-understood structs :] - -# a connection from a geo's direction to another geo's -geo_connection = Struct( - initial_direction=Int32ul, # 0-4, up-right-down-left typically. i.e. geo(0.0, 0.0).direction(1) = geo(100.0, 0.0) - destination_geo=Int32ul, - destination_direction=Int32ul, # see initial_direction -) - -# a list of connections for a geo (essentially, the allowed directions of travel) -geo_connections = Struct( - directions=PrefixedArray(Int32ul, Int32ul), - connections=PrefixedArray(Int32ul, geo_connection), -) - -geo_connections_sr = Struct( - unk0=Byte, - unk1=Byte, - unk2=Byte, # 0? - gcs=geo_connections, -) - -# idk -Struct1 = Struct( - unk0=Int32ul, - unk1=CVector2D, - unk2=CVector2D, -) - -# special paths entities can take that ignore default connections (i.e. chozo robot jumps or wall-traveling enemies). -# emmi's are in a separate structure. -# hard-coded to specific entity's sName -NavigablePath = Struct( - path=PrefixedArray(Int32ul, Int32ul), -) - -EZ_Element = Struct( - el=Int32ul, - unk1=PrefixedArray(Int32ul, Int32ul), -) - -EmmyZone_inner = Struct( - elements=PrefixedArray(Int32ul, EZ_Element), -) - -# reference to an "LS_EmmyZone" typically -EmmyZone = Struct( - elements=Int32ul, - el1=EmmyZone_inner, -) - -# a specific traversal (ie "emmi can swing across this gap") -Traversal = Struct( - unk0=Array(4, Int32ul), # weird shit - initial_position=CVector2D, - final_position=CVector2D, - initial_rotation=CVector3D, - final_rotation=CVector3D, - unk12=StrId, - unk13=StrId, - action_name=StrId, # specific action in the bmslink - unk14=Flag, - unk15=Float, - unk16=Float, - _unk17=PrefixedArray(Int32ul, Int32ul), # i think this is geos? - _unk18=PrefixedArray(Int32ul, CVector2D), # these might also be geos but i just used vectors since its easier - _unk19=PrefixedArray(Int32ul, CVector2D), - _unk20=PrefixedArray(Int32ul, CVector2D), - unk21=Flag, - unk22=Flag, - unk23=Float, +### +### Lots of notes here because being able to regenerate with added or +### moved assets would likely reduce bugginess and lag spikes, +### as well as allow custom level geometry + +### quick glossary: +### Point / Valid Point: A vertex, basically. +### Area: A navigable area defined by 3-4 Points. Prefers to be rectangles. +### Edge: The line between consecutive vertices in an Area. Areas sharing an Edge each have their own. +### Zone: A group of Areas. usually contiguous unless another actor exists there +### i.e. each door has a Zone, each shield has a Zone, morph tunnels are Zones, rooms are Zones + +# A specific edge for a specific area +# Edge i of Area a is the line between a.vertices[i] and a.vertices[i+1] +AreaEdge = Struct( + "area" / Int32ul, + "edge" / Int32ul, +) + +# a region where the player can move and enemies can pathfind +# verts are counterclockwise and the first edge is usually the "floor" +# adjacent_edges[n] represents the nth edge's EdgeData from the other area's side +DreadArea = Struct( + "vertices" / make_vector(Int32ul), + "adjacent_edges" / make_dict(AreaEdge, Int32ul), +) + +MSRArea = Struct( + "unk0" / Byte, + "unk1" / Byte, + "unk2" / Byte, # 0? + "area" / DreadArea, +) + +# A node in the search tree +# if the point is inside the box defined by (min, max) it goes to the next entry +# if the point is outside the box it goes to the fail_index +# leaves always correspond with the bounds of an Area and have inverse behavior, +# i.e. if it is in the bounds it accesses navmesh.areas[fail_index] +# and if it outside the bounds it goes to the next index +# TODO research how the divisions are made so we can regenerate a navmesh +SearchTreeNode = Struct( + "test" + / ByteSwapped( + BitStruct( + "on_pass" / Flag, + "goto" / BitsInteger(31), + ) + ), + "min" / CVector2D, + "max" / CVector2D, +) + +# a list of aNavmeshGeos within a LogicShape from the level data +LogicShapeValidPoints = Struct( + "points" / make_vector(Int32ul), +) + +LogicShapesEdges_Area = Struct( + "index" / Int32ul, + "included_edges" / make_vector(Int32ul), +) + +# a list of Edges within a LogicShape from the level data +# typically used for emmi zone and LS_Forbidden +LogicShapesEdges = Struct( + "num_edges" / Int32ul, # TODO adapter to rebuild this + "areas" / make_vector(LogicShapesEdges_Area), +) + +# a specific traversal (ie "emmi can swing across this gap"/"yamplot can jump down here") +# naming maybe wrong, essentially a dynamic smart link rule applied to an explicit location on the mesh. +StaticSmartLink = Struct( + "unk0" / Int32ul, # doesn't seem to be an area/edge + "unk1" / Int32ul, + "end_edge" / AreaEdge, + "initial_position" / CVector2D, + "final_position" / CVector2D, + "initial_rotation" / CVector3D, + "final_rotation" / CVector3D, + "unk12" / StrId, + "unk13" / StrId, + "action_name" / StrId, # specific action in the bmslink + "unk14" / Flag, + "unk15" / Float, + "unk16" / Float, + "areas_in_link" / make_vector(Int32ul), # all areas it passes through + "edges_in_link" / make_vector(AreaEdge), # same as the above, but with edges (possibly an unk instead) + "start_areas" / make_vector(AreaEdge), # empty or first half of above list + "end_areas" / make_vector(AreaEdge), # empty or second half of above list + "unk21" / Flag, + "unk22" / Flag, + "unk23_maybe_weight" / Float, ) # another prefixed array with an unknown parameter. I think maybe this is done based on the area/room the emmy is in? -EmmyAreaTraversal = Struct( - actions=PrefixedArray(Int32ul, Traversal), +SmartLinksFromArea = Struct( + "start_area" / Int32ul, + "links" / PrefixedArray(Int32ul, StaticSmartLink), ) -# emmy-specific traversal (ie where they can hop up on ceilings). -# includes a bmslink reference and refers to specific actions. -EmmyTraversals = Struct( - bmslink=StrId, - traversals=make_dict(EmmyAreaTraversal, Int32ul), +# Actor-specific SmartLink paths +DynamicSmartLinkRule = Struct( + "source_bmslink" / StrId, # source SmartLink file? + "smartlinks" / make_vector(SmartLinksFromArea), ) -PAction = Struct( - unk0=Int32ul, - action=Traversal, +ZoneSmartLinkAction = Struct( + "area" / Int32ul, + "action" / StaticSmartLink, ) -PropActions = Struct( - name=StrId, - bmslink=StrId, - actions=PrefixedArray(Int32ul, PAction), +SmartLinkRule = Struct( + "name" / StrId, + "bmslink" / StrId, + "actions" / PrefixedArray(Int32ul, ZoneSmartLinkAction), ) -# actions around certain props like buttons +# actions around certain props like water Prop = Struct( - actions=PrefixedArray(Int32ul, PrefixedArray(Int32ul, PropActions)), + "actions" / PrefixedArray(Int32ul, PrefixedArray(Int32ul, SmartLinkRule)), ) # a parameter for actor. seems to be sublayers of the navmesh. -Actor_unk1_param = Struct( - sName=StrId, - unk=Int32ul, +ActorStage = Struct( + "actor_name" / StrId, # sometimes raw actor sometimes actor_sname_st01 + "stage_idx" / Int32ul, # likely something to do with the collider stage stuff in bmsad? ) # info on an actor -Actor = Struct( - unk0=Byte, - unk1=PrefixedArray(Int32ul, Actor_unk1_param), - # these three are either 00 00 FF or XX 00 YY, where the actor is TILEGROUP.uGroupId XX as TILEGROUP.aGridTiles[YY] - unk2=Byte, - unk3=Byte, - unk4=Byte, - coordinates=CVector2D, - geos=PrefixedArray(Int32ul, Int32ul), -) - -IntIntStruct = Struct( - unk1=Int32ul, - unk2=Int32ul, +ZoneData = Struct( + # guess, typically true for "small" areas where you'd be expected to morph + # like "hidey holes" in emmi zones, or regular tunnels. though some things + # like the "cross bomb" areas above crumbles aren't. + "is_morph_tunnel" / Flag, + "actor_stages" / make_vector(ActorStage), + "tile_group_id" / Byte, # 00 for non-tiles + "unk3" / Const(b"\x00"), + "tile_group_index" / Byte, # FF for non-tiles + "center" / CVector2D, + "areas" / make_vector(Int32ul), ) -Struct3 = Struct( - unk1=Int32ul, - unk2=Int32ul, - unk3=Int32ul, - pos=CVector2D, - unk4=PrefixedArray(Int32ul, IntIntStruct), # seems to be a geo and an enum +ZoneEdgeData = Struct( + "zone" / Int32ul, # "destination" zone index, the one connected to the edges + "main_edge" / AreaEdge, # zone the pos is in? + "pos" / CVector2D, # center of edge + "all_edges" / make_vector(AreaEdge), # all edges, including main_edge ) -Struct2 = Struct( - unk0=Int32ul, - unk1=PrefixedArray(Int32ul, Struct3), +ZoneEdges = Struct( + "zone" / Int32ul, # "source" zone, i.e. where the areas are + "edges" / make_vector(ZoneEdgeData), ) sr_unk_struct = Struct( - bound_start=CVector2D, bound_end=CVector2D, unk1=Int32ul, const0=Int32ul, unk2=Int32ul, unk3=Int32ul + "bound_start" / CVector2D, + "bound_end" / CVector2D, + "unk1" / Int32ul, + "const0" / Int32ul, # TODO what const? + "unk2" / Int32ul, + "unk3" / Int32ul, ) BMSNAV_SR = Struct( - _magic=Const(b"MNAV"), - version=Const(0x000C0001, Hex(Int32ul)), - aNavmeshGeos=PrefixedArray(Int32ul, CVector2D), - geo_connections=PrefixedArray(Int32ul, geo_connections_sr), - unk1=PrefixedArray(Int32ul, Struct1), - navigable_paths=make_dict(NavigablePath), # contains additional paths for certain enemies (ie chozo soldiers) - unk2=make_dict(PrefixedArray(Int32ul, sr_unk_struct)), - _eof=Terminated, -) -BMSNAV = Struct( - _magic=Const(b"MNAV"), - version=VersionAdapter("2.3.0"), - aNavmeshGeos=PrefixedArray(Int32ul, CVector2D), - # giant list of all of the navmesh geos in the scenario. referenced by index all over the rest of the format. - geo_connections=PrefixedArray(Int32ul, geo_connections), # maybe mapping the geo connections? - unk1=PrefixedArray(Int32ul, Struct1), - # seems like these contain bounding boxes of some sort, antidote thought maybe octants - navigable_paths=make_dict(NavigablePath), # contains additional paths for certain enemies (ie chozo soldiers) - emmy_zones=make_dict(EmmyZone), # references "LS_EmmyZone" and maybe other logicshapes - unk_arr=PrefixedArray(Int32ul, Int32ul), # i suspect this is an array but not used anywhere. - emmy_actions=make_dict(EmmyTraversals), - # seems to contain additional emmi navigation methods, and links to bmslink. - props=make_dict(Prop), # seems to change emmi animations around specific props (ie water button) - actors=make_dict(Actor), # info on actors' navmeshes - unk2=PrefixedArray(Int32ul, Struct2), # idk on this one + "_magic" / Const(b"MNAV"), + "version" / VersionAdapter("1.12.0"), + "aNavmeshGeos" / PrefixedArray(Int32ul, CVector2D), + "areas" / PrefixedArray(Int32ul, MSRArea), + "search_tree" / PrefixedArray(Int32ul, SearchTreeNode), + "logic_shapes" / make_dict(LogicShapeValidPoints), # vertices in certain LS's + "unk2" / make_dict(PrefixedArray(Int32ul, sr_unk_struct)), + Terminated, +) + +BMSNAV_DREAD = Struct( + "_magic" / Const(b"MNAV"), + "version" / VersionAdapter("2.3.0"), + "points" / make_vector(CVector2D), + "areas" / make_vector(DreadArea), + "search_tree" / make_vector(SearchTreeNode), + "logic_shapes_valid_points" / make_dict(LogicShapeValidPoints), # vertices in certain LS's + "logic_shapes_edges" / make_dict(LogicShapesEdges), # edges in certain LS's + "unk_arr" / make_vector(Int32ul), + "dynamic_smartlink_rules" / make_dict(DynamicSmartLinkRule), # likely converted from bmslink format + "props" / make_dict(Prop), # seems to change emmi animations around specific zones (like water) + "zones" / make_dict(ZoneData), + "zone_edges" / make_vector(ZoneEdges), + Terminated, ).compile() @@ -181,5 +224,5 @@ class Bmsnav(BaseResource): def construct_class(cls, target_game: Game) -> Construct: return { Game.SAMUS_RETURNS: BMSNAV_SR, - Game.DREAD: BMSNAV, + Game.DREAD: BMSNAV_DREAD, }[target_game] diff --git a/src/mercury_engine_data_structures/formats/collision.py b/src/mercury_engine_data_structures/formats/collision.py index f733688b..162cd930 100644 --- a/src/mercury_engine_data_structures/formats/collision.py +++ b/src/mercury_engine_data_structures/formats/collision.py @@ -1,7 +1,7 @@ from __future__ import annotations import construct -from construct import Array, Flag, Hex, Int16ul, Rebuild, Struct +from construct import Array, BitsInteger, BitStruct, ByteSwapped, Flag, Hex, Rebuild, Struct from mercury_engine_data_structures import game_check from mercury_engine_data_structures.common_types import CVector2D, CVector3D, Float, UInt, make_vector @@ -30,8 +30,13 @@ CollisionPoly = game_check.is_at_most(Game.SAMUS_RETURNS, CollisionPolySR, CollisionPolyDread) BinarySearchTree = Struct( - binary_search_index1=Int16ul, - binary_search_index2=Int16ul, + test=ByteSwapped( + BitStruct( + "is_leaf" / Flag, + "pass" / BitsInteger(15), + "fail" / BitsInteger(16), + ) + ), boundings=Array(4, Float), )