def __init__(self, source=None, **kwargs): """Most of these floats have been mapped out.""" self._wind_vector_min = Vector3.zero() self._wind_vector_max = Vector3.zero() self._wind_swing_cycles = [0.0, 0.0, 0.0, 0.0] self._wind_swing_powers = [0.0, 0.0, 0.0, 0.0] super().__init__(source=source, **kwargs)
def __init__( self, position: Vector3 = None, bone_weights: VertexBoneWeights = None, bone_indices: VertexBoneIndices = None, normal: Vector3 = None, normal_w: int = None, uvs: list[Vector3] = None, tangents: list[Vector4] = None, bitangent: Vector4 = None, colors: list[ColorRGBA] = None, ): self.position = Vector3.zero() if position is None else position self.bone_weights = VertexBoneWeights( ) if bone_weights is None else bone_weights self.bone_indices = VertexBoneIndices( ) if bone_indices is None else bone_indices self.normal = Vector3.zero() if normal is None else normal self.normal_w = 0 if normal_w is None else normal_w self.uvs = [] if uvs is None else uvs self.tangents = [] if tangents is None else tangents self.bitangent = Vector4.zero() if bitangent is None else bitangent self.colors = [] if colors is None else colors self.raw = b"" self.uv_queue = [] # type: list[Vector3] self.tangent_queue = [] # type: list[Vector4] self.color_queue = [] # type: list[ColorRGBA]
class MSBEntryEntityCoordinates(MSBEntryEntity, abc.ABC): """Subclass of MSBEntryEntity with `translate` and `rotate` fields, and `rotate_in_world` method. Inherited by both `MSBPart` and `MSBRegion`). """ FIELD_INFO = MSBEntryEntity.FIELD_INFO | { "translate": MapFieldInfo( "Translate", Vector3, Vector3.zero(), "3D coordinates of the part's position. Note that the anchor of the part is usually at its base.", ), "rotate": MapFieldInfo( "Rotate", Vector3, Vector3.zero(), "Euler angles for part rotation around its local X, Y, and Z axes.", ), } translate: Vector3 rotate: Vector3 def __init__(self, source=None, **kwargs): self._translate = Vector3.zero() self._rotate = Vector3.zero() super().__init__(source=source, **kwargs) def apply_rotation( self, rotation: tp.Union[Matrix3, Vector3, list, tuple, int, float], pivot_point=(0, 0, 0), radians=False, ): """Modify entity `translate` and `rotate` by rotating entity around some `pivot_point` in world coordinates. Default `pivot_point` is the world origin (0, 0, 0). Default rotation units are degrees. """ rotation = resolve_rotation(rotation, radians) pivot_point = Vector3(pivot_point) self._rotate = (rotation @ Matrix3.from_euler_angles(self.rotate)).to_euler_angles() self._translate = (rotation @ (self.translate - pivot_point)) + pivot_point @property def translate(self): return self._translate @translate.setter def translate(self, value): self._translate = Vector3(value) @property def rotate(self): return self._rotate @rotate.setter def rotate(self, value): if isinstance(value, (int, float)): self._rotate = Vector3(0, value, 0) else: self._rotate = Vector3(value)
def add_point_from_player_position(self, sequence_field: str, field_nickname: str): if not self.linker.is_hooked: if ( self.CustomDialog( title="Cannot Read Memory", message="Game has not been hooked. Would you like to try hooking into the game now?", default_output=0, cancel_output=1, return_output=0, button_names=("Yes, hook in", "No, forget it"), button_kwargs=("YES", "NO"), ) == 1 ): return if self.linker.runtime_hook(): # Call this function again. return self.add_point_from_player_position(sequence_field, field_nickname) return field_dict = self.get_selected_field_dict() sequence = field_dict[sequence_field] i = -1 for i, existing_region_name in enumerate(sequence): if existing_region_name is None: break if i == -1: # Sequence is full. self.CustomDialog( title="Cannot Add New Point", message=f"Field '{sequence_field}' of entry '{field_dict.name}' cannot hold any more Points.", ) return point_name = f"{field_dict.name} {field_nickname} {i}" try: player_x = self.linker.get_game_value("player_x") player_y = self.linker.get_game_value("player_y") player_z = self.linker.get_game_value("player_z") new_translate = Vector3(player_x, player_y, player_z) new_rotate_y = math.degrees(self.linker.get_game_value("player_angle")) except MemoryHookError as e: _LOGGER.error(str(e), exc_info=True) self.CustomDialog( title="Cannot Read Memory", message=f"An error occurred when trying to copy player position (see log for full traceback):\n\n" f"{str(e)}\n\n" f"If this error doesn't seem like it can be solved (e.g. did you close the game after\n" f"hooking into it?) please inform Grimrukh.", ) return self.get_selected_msb().regions.new_point( name=point_name, translate=new_translate, rotate=Vector3(0, new_rotate_y, 0), ) sequence[i] = point_name self.refresh_fields()
def __init__(self): """Subclass of MSBEntryEntity with `translate` and `rotate` fields, and `rotate_in_world` method. Inherited by both `MSBPart` and `MSBRegion`). """ super().__init__() self._translate = Vector3.zero() self._rotate = Vector3.zero()
def __init__(self): self.position = Vector3.zero() self.bone_weights = VertexBoneWeights() self.bone_indices = VertexBoneIndices() self.normal = Vector3.zero() self.normal_w = 0 self.uvs = [] # type: list[Vector3] self.tangents = [] # type: list[Vector4] self.bitangent = Vector4.zero() self.colors = [] # type: list[Color]
def __init__(self): self.position = Vector3.zero() self.bone_weights = VertexBoneWeights() self.bone_indices = VertexBoneIndices() self.normal = Vector3.zero() self.normal_w = 0 self.uvs = [] # type: tp.List[Vector3] self.tangents = [] # type: tp.List[Vector4] self.bitangent = Vector4.zero() self.colors = [] # type: tp.List[ColorRGBA] self.raw = b"" self.uv_queue = [] # type: tp.List[Vector3] self.tangent_queue = [] # type: tp.List[Vector4] self.color_queue = [] # type: tp.List[ColorRGBA]
def unpack(self, msb_reader: BinaryReader): part_offset = msb_reader.position header = msb_reader.unpack_struct(self.PART_HEADER_STRUCT) if header["__part_type"] != self.ENTRY_SUBTYPE: raise ValueError(f"Unexpected part type enum {header['part_type']} for class {self.__class__.__name__}.") self._instance_index = header["_instance_index"] self._model_index = header["_model_index"] self._part_type_index = header["_part_type_index"] for transform in ("translate", "rotate", "scale"): setattr(self, transform, Vector3(header[transform])) self._draw_groups = int_group_to_bit_set(header["__draw_groups"], assert_size=8) self._display_groups = int_group_to_bit_set(header["__display_groups"], assert_size=8) self._backread_groups = int_group_to_bit_set(header["__backread_groups"], assert_size=8) self.description = msb_reader.unpack_string( offset=part_offset + header["__description_offset"], encoding="utf-16-le", ) self.name = msb_reader.unpack_string( offset=part_offset + header["__name_offset"], encoding="utf-16-le", ) self.sib_path = msb_reader.unpack_string( offset=part_offset + header["__sib_path_offset"], encoding="utf-16-le", ) msb_reader.seek(part_offset + header["__base_data_offset"]) base_data = msb_reader.unpack_struct(self.PART_BASE_DATA_STRUCT) self.set(**base_data) msb_reader.seek(part_offset + header["__type_data_offset"]) self.unpack_type_data(msb_reader) self._unpack_gparam_data(msb_reader, part_offset, header) self._unpack_scene_gparam_data(msb_reader, part_offset, header)
def rotate_all_in_world( self, rotation: tp.Union[Matrix3, Vector3, list, tuple, int, float], pivot_point=(0, 0, 0), radians=False, selected_entries=None, ): """Rotate every Part and Region in the map (or a selection given in `selected_entry_names`) around the given pivot by the given Euler angles coordinate system, modifying both `translate` and `rotate`. The pivot defaults to the world origin. """ if isinstance(rotation, (int, float)): rotation = Matrix3.from_euler_angles( 0.0, rotation, 0.0) # single rotation value interpreted as Y rotation elif isinstance(rotation, (Vector3, list, tuple)): rotation = Matrix3.from_euler_angles(rotation) elif not isinstance(rotation, Matrix3): raise TypeError( "`rotation` must be a Matrix3, Vector3/list/tuple, or int/float (for Y rotation only)." ) pivot_point = Vector3(pivot_point) for p in self.parts: if selected_entries is None or p in selected_entries: p.rotate_in_world(rotation, pivot_point=pivot_point, radians=radians) for r in self.regions: if selected_entries is None or r in selected_entries: r.rotate_in_world(rotation, pivot_point=pivot_point, radians=radians)
def rotate_all_in_world( self, rotation: tp.Union[Matrix3, Vector3, list, tuple, int, float], pivot_point=(0, 0, 0), radians=False, selected_entries=(), ): """Rotate every Part and Region in the map around the given `pivot_point` by the Euler angles specified by `rotation`, modifying both `.rotate` and (unless equal to `pivot_point`) `.translate` for each entry. Args: rotation: Euler angles, as specified by `(x, y, z)`, an Euler rotation matrix, or a single value to apply simple `y` rotation only. pivot_point: point around with `rotation` will be applied. Defaults to world origin, `(0, 0, 0)`. radians: if True, given `rotation` is in radians; degrees otherwise. Defaults to `False` (degrees). selected_entries: if not empty, move only these given entries. Each element in this sequence can be an `MSBEntry` instance or the name (if unique) of a Part or Region. """ selected_entries = self.resolve_entries_list(selected_entries, entry_types=("parts", "regions")) rotation = resolve_rotation(rotation) pivot_point = Vector3(pivot_point) for part in self.parts: if not selected_entries or part in selected_entries: part.apply_rotation(rotation, pivot_point=pivot_point, radians=radians) for region in self.regions: if not selected_entries or region in selected_entries: region.apply_rotation(rotation, pivot_point=pivot_point, radians=radians)
def parse_Vector(self, queue: deque) -> Vector3: self._assert(queue, "Vector") self._assert(queue, "") self._assert(queue, "A", optional=True) x = queue.popleft() y = queue.popleft() z = queue.popleft() return Vector3(x, y, z)
def get_absolute_translate(self, bones: list[Bone]) -> Vector3: """Accumulates parents' translates and rotates.""" absolute_translate = Vector3.zero() rotate = Matrix3.identity() indent = "" for bone in self.get_all_parents(bones): absolute_translate += rotate @ bone.translate rotate @= Matrix3.from_euler_angles(bone.rotate, radians=True) indent += " " return absolute_translate
def apply_rotation( self, rotation: tp.Union[Matrix3, Vector3, list, tuple, int, float], pivot_point=(0, 0, 0), radians=False, ): """Modify entity `translate` and `rotate` by rotating entity around some `pivot_point` in world coordinates. Default `pivot_point` is the world origin (0, 0, 0). Default rotation units are degrees. """ rotation = resolve_rotation(rotation, radians) pivot_point = Vector3(pivot_point) self._rotate = (rotation @ Matrix3.from_euler_angles(self.rotate)).to_euler_angles() self._translate = (rotation @ (self.translate - pivot_point)) + pivot_point
def _reload_warp(self): if not self.linker.is_hooked: if (self.CustomDialog( title="Cannot Read Position", message= "Game has not been hooked. Would you like to try hooking into the game now?", default_output=0, cancel_output=1, return_output=0, button_names=("Yes, hook in", "No, forget it"), button_kwargs=("YES", "NO"), ) == 1): return if not self.linker.runtime_hook(): return map_id = self.linker.get_current_map_id() if map_id is None: return self.CustomDialog( "Game Not Loaded", "Could not detect player in any game map.") player_start_id = map_id[0] * 100000 + map_id[ 1] * 10000 + self.RELOAD_WARP_PLAYER_START_SUFFIX request_warp_flag_id = 10000000 + map_id[0] * 100000 + map_id[ 1] * 10000 + self.RELOAD_WARP_FLAG_SUFFIX current_map = self.project.maps.GET_MAP(map_id) current_msb_path = self.project.get_game_path_of_data_type( "maps") / f"{current_map.msb_file_stem}.msb" current_msb = MSB(current_msb_path) try: player_start = current_msb.get_entry_by_entity_id(player_start_id) except KeyError: return self.CustomDialog( "Reload Warp Failed", f"MSB '{current_msb_path.stem}' has no Player Start with entity ID {player_start_id}." ) if not isinstance(player_start, MSBPlayerStart): return self.CustomDialog( "Reload Warp Failed", f"MSB '{current_msb_path.stem}' has an entity ID {player_start_id}, but it is not a Player Start." ) player_x = self.linker.get_game_value("player_x") player_y = self.linker.get_game_value("player_y") player_z = self.linker.get_game_value("player_z") player_start.translate = Vector3(player_x, player_y, player_z) player_start.rotate.y = math.degrees( self.linker.get_game_value("player_angle")) current_msb.write() self.linker.enable_flag(request_warp_flag_id) # TODO: Remove print( f"Wrote MSB {current_msb_path.name} with altered Player Start {player_start_id} " f"and enabled flag {request_warp_flag_id}")
def unpack(self, msb_buffer): region_offset = msb_buffer.tell() base_data = self.REGION_STRUCT.unpack(msb_buffer) self.name = read_chars_from_buffer( msb_buffer, offset=region_offset + base_data["name_offset"], encoding="shift-jis" ) self._region_index = base_data["region_index"] self.translate = Vector3(base_data["translate"]) self.rotate = Vector3(base_data["rotate"]) self.check_null_field(msb_buffer, region_offset + base_data["unknown_offset_1"]) self.check_null_field(msb_buffer, region_offset + base_data["unknown_offset_2"]) if base_data["type_data_offset"] != 0: msb_buffer.seek(region_offset + base_data["type_data_offset"]) self.unpack_type_data(msb_buffer) msb_buffer.seek(region_offset + base_data["entity_id_offset"]) self.entity_id = struct.unpack("i", msb_buffer.read(4))[0] return region_offset + base_data["entity_id_offset"]
def copy_player_position(self, translate=False, rotate=False, y_offset=0.0): if not translate and not rotate: raise ValueError( "At least one of `translate` and `rotate` should be True.") new_translate = None new_rotate_y = None try: if translate: player_x = self.linker.get_game_value("player_x") player_y = self.linker.get_game_value("player_y") + y_offset player_z = self.linker.get_game_value("player_z") new_translate = Vector3(player_x, player_y, player_z) if rotate: new_rotate_y = math.degrees( self.linker.get_game_value("player_angle")) except ConnectionError: if (self.CustomDialog( title="Cannot Read Memory", message= "Runtime hooks are not available. Would you like to try hooking into the game now?", default_output=0, cancel_output=1, return_output=0, button_names=("Yes, hook in", "No, forget it"), button_kwargs=("YES", "NO"), ) == 1): return if self.linker.runtime_hook(): return self.copy_player_position(translate=translate, rotate=rotate, y_offset=y_offset) return except MemoryHookError as e: _LOGGER.error(str(e), exc_info=True) self.CustomDialog( title="Cannot Read Memory", message= f"An error occurred when trying to copy player position (see log for full traceback):\n\n" f"{str(e)}\n\n" f"If this error doesn't seem like it can be solved (e.g. did you close the game after\n" f"hooking into it?) please inform Grimrukh.", ) return field_dict = self.get_selected_field_dict() if translate: field_dict["translate"] = new_translate if rotate: field_dict["rotate"].y = new_rotate_y self.refresh_fields()
def unpack(self, msb_reader: BinaryReader): region_offset = msb_reader.position base_data = msb_reader.unpack_struct(self.REGION_STRUCT) self.name = msb_reader.unpack_string( offset=region_offset + base_data["name_offset"], encoding=self.NAME_ENCODING, ) self._region_index = base_data["__region_index"] self.translate = Vector3(base_data["translate"]) self.rotate = Vector3(base_data["rotate"]) self.check_null_field(msb_reader, region_offset + base_data["unknown_offset_1"]) self.check_null_field(msb_reader, region_offset + base_data["unknown_offset_2"]) if base_data["type_data_offset"] != 0: msb_reader.seek(region_offset + base_data["type_data_offset"]) self.unpack_type_data(msb_reader) msb_reader.seek(region_offset + base_data["entity_id_offset"]) self.entity_id = msb_reader.unpack_value("i") return region_offset + base_data["entity_id_offset"]
def move_map( self, start_translate: tp.Union[Vector3, list, tuple] = None, end_translate: tp.Union[Vector3, list, tuple] = None, start_rotate: tp.Union[Vector3, list, tuple, int, float, None] = None, end_rotate: tp.Union[Vector3, list, tuple, int, float, None] = None, selected_entries=(), ): """Move everything with a transform in this `MSB` relative to an initial and a final reference point. Args: start_translate: initial `(x, y, z)` translate of initial reference point. end_translate: final `(x, y, z)` translate of final reference point. start_rotate: initial `(x, y, z)` rotate of initial reference point, or simply `y` if a number is given. end_rotate: final `(x, y, z)` rotate of final reference point, or simply `y` if a number is given. selected_entries: if not empty, move only these given entries. Each element in this sequence can be an `MSBEntry` instance or the name (if unique) of a Part or Region. Optionally, move only a subset of entry names given in `selected_entry_names`. """ selected_entries = self.resolve_entries_list(selected_entries, entry_types=("parts", "regions")) start_translate = Vector3(start_translate) end_translate = Vector3(end_translate) start_rotate = Vector3(0, start_rotate, 0) if isinstance(start_rotate, (int, float)) else Vector3(start_rotate) end_rotate = Vector3(0, end_rotate, 0) if isinstance(end_rotate, (int, float)) else Vector3(end_rotate) # Compute global rotation matrix required to get from `start_rotate` to `end_rotate`. m_start_rotate = Matrix3.from_euler_angles(start_rotate) m_end_rotate = Matrix3.from_euler_angles(end_rotate) m_world_rotation = m_end_rotate @ m_start_rotate.T # Apply global rotation to start point to determine required global translation. translation = end_translate - (m_world_rotation @ start_translate) # type: Vector3 self.rotate_all_in_world(m_world_rotation, selected_entries=selected_entries) self.translate_all(translation, selected_entries=selected_entries)
class MSBMapOffsetEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.MapOffset EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("translate", "3f"), ("rotate_y", "f"), ) FIELD_INFO = MSBEvent.FIELD_INFO | { "translate": MapFieldInfo( "Translate", Vector3, Vector3.zero(), "Vector of (x, y, z) coordinates of map offset.", ), "rotate_y": MapFieldInfo( "Y Rotation", float, 0.0, "Euler angle of rotation around the Y (vertical) axis.", ), } FIELD_ORDER = ( "translate", "rotate_y", ) translate: Vector3 rotate_y: float def __init__(self, source=None, **kwargs): self._translate = Vector3.zero() super().__init__(source=source, **kwargs) @property def translate(self): return self._translate @translate.setter def translate(self, value): self._translate = Vector3(value)
def unpack(self, msb_buffer): part_offset = msb_buffer.tell() header = self.PART_HEADER_STRUCT.unpack(msb_buffer) if header["__part_type"] != self.ENTRY_SUBTYPE: raise ValueError(f"Unexpected part type enum {header['part_type']} for class {self.__class__.__name__}.") self._model_index = header["_model_index"] self._part_type_index = header["_part_type_index"] for transform in ("translate", "rotate", "scale"): setattr(self, transform, Vector3(getattr(header, transform))) self._draw_groups = int_group_to_bit_set(header["__draw_groups"], assert_size=4) self._display_groups = int_group_to_bit_set(header["__display_groups"], assert_size=4) self.name = read_chars_from_buffer( msb_buffer, offset=part_offset + header["__name_offset"], encoding=self.NAME_ENCODING ) self.sib_path = read_chars_from_buffer( msb_buffer, offset=part_offset + header["__sib_path_offset"], encoding=self.NAME_ENCODING ) msb_buffer.seek(part_offset + header["__base_data_offset"]) base_data = self.PART_BASE_DATA_STRUCT.unpack(msb_buffer) self.set(**base_data) msb_buffer.seek(part_offset + header["__type_data_offset"]) self.unpack_type_data(msb_buffer)
def copy_player_position(self, translate=False, rotate=False, draw_parent_name=False, y_offset=0.0): if not translate and not rotate and not draw_parent_name: raise ValueError( "At least one of `translate`, `rotate`, and `draw_parent_name` should be True." ) new_translate = None new_rotate_y = None new_collision = None if not self.linker.is_hooked: if (self.CustomDialog( title="Cannot Read Memory", message= "Game has not been hooked. Would you like to try hooking into the game now?", default_output=0, cancel_output=1, return_output=0, button_names=("Yes, hook in", "No, forget it"), button_kwargs=("YES", "NO"), ) == 1): return if self.linker.runtime_hook(): # Call this function again. return self.copy_player_position( translate=translate, rotate=rotate, draw_parent_name=draw_parent_name, y_offset=y_offset, ) return try: if translate: player_x = self.linker.get_game_value("player_x") player_y = self.linker.get_game_value("player_y") + y_offset player_z = self.linker.get_game_value("player_z") new_translate = Vector3(player_x, player_y, player_z) if rotate: new_rotate_y = math.degrees( self.linker.get_game_value("player_angle")) if draw_parent_name: map_prefix = self.map_choice_id[:6] # e.g. "m10_02" display_group_ints = self.linker.get_game_value( f"{map_prefix}_display_groups") display_groups = int_group_to_bit_set( display_group_ints, assert_size=4) # TODO: Other game sizes. if not display_groups: self.CustomDialog( title="No Collision Found", message= f"No display groups in {self.map_choice_id} are currently active.\n" f"Are you sure the player is currently standing in this map? (No changes made.)", ) return search = [ col for col in self.get_selected_msb().parts.Collisions if col.display_groups == display_groups ] if len(search) > 1: # Find lowest-index collision. new_collision = min(search, key=lambda c: int(c.model_name[1:5])) elif search: new_collision = search[0] else: self.CustomDialog( title="No Collision Found", message= f"Could not find any collisions that match current player display groups: " f"{display_groups}. No changes made.", ) return except MemoryHookError as e: _LOGGER.error(str(e), exc_info=True) self.CustomDialog( title="Cannot Read Memory", message= f"An error occurred when trying to copy player position (see log for full traceback):\n\n" f"{str(e)}\n\n" f"If this error doesn't seem like it can be solved (e.g. did you close the game after\n" f"hooking into it?) please inform Grimrukh.", ) return field_dict = self.get_selected_field_dict() if translate: field_dict["translate"] = new_translate if rotate: field_dict["rotate"].y = new_rotate_y if draw_parent_name: field_dict["draw_parent_name"] = new_collision.name self.refresh_fields()
def rotate(self, value): if isinstance(value, (int, float)): self._rotate = Vector3(0, value, 0) else: self._rotate = Vector3(value)
def translate(self, value): self._translate = Vector3(value)
def __init__(self, source=None, **kwargs): self._translate = Vector3.zero() self._rotate = Vector3.zero() super().__init__(source=source, **kwargs)
class MSBPart( BaseMSBPart, MSB_Scale, MSB_ModelName, MSB_DrawParent, MSB_DrawGroups, MSB_DisplayGroups, MSB_BackreadGroups, abc.ABC, ): PART_HEADER_STRUCT = BinaryStruct( ("__description_offset", "q"), ("__name_offset", "q"), ("_instance_index", "i"), # TK says "Unknown; appears to count up with each instance of a model added." ("__part_type", "i"), ("_part_type_index", "i"), ("_model_index", "i"), ("__sib_path_offset", "q"), ("translate", "3f"), ("rotate", "3f"), ("scale", "3f"), ("__draw_groups", "8I"), ("__display_groups", "8I"), ("__backread_groups", "8I"), "4x", ("__base_data_offset", "q"), ("__type_data_offset", "q"), ("__gparam_data_offset", "q"), ("__scene_gparam_data_offset", "q"), ) PART_BASE_DATA_STRUCT = BinaryStruct( ("entity_id", "i"), ("base_unk_x04_x05", "b"), ("base_unk_x05_x06", "b"), ("base_unk_x06_x07", "b"), ("base_unk_x07_x08", "b"), "4x", ("lantern_id", "b"), ("lod_id", "b"), ("base_unk_x0e_x0f", "b"), ("base_unk_x0f_x10", "b"), ) NAME_ENCODING = "utf-16-le" DRAW_GROUP_COUNT = 256 DISPLAY_GROUP_COUNT = 256 BACKREAD_GROUP_COUNT = 256 FIELD_INFO = BaseMSBPart.FIELD_INFO | { "sib_path": MapFieldInfo( "SIB Path", str, "", "Internal path to SIB placeholder file for part.", ), "scale": MapFieldInfo( "Scale", Vector3, Vector3.ones(), "Scale of part. Only works for Map Pieces and Objects.", ), "draw_groups": MapFieldInfo( "Draw Groups", list, set(range(DRAW_GROUP_COUNT)), "Draw groups of part. This part will be drawn when the corresponding display group is active.", ), "display_groups": MapFieldInfo( "Display Groups", list, set(range(DISPLAY_GROUP_COUNT)), "Display groups are present in all MSB Parts, but only function for collisions.", ), "backread_groups": MapFieldInfo( "Backread Groups", list, set(range(BACKREAD_GROUP_COUNT)), "Backread groups are present in all MSB Parts, but only function for collisions (probably).", ), "base_unk_x04_x05": MapFieldInfo( "Unknown Base [04-05]", int, -1, "Unknown base data integer.", ), "base_unk_x05_x06": MapFieldInfo( "Unknown Base [05-06]", int, -1, "Unknown base data integer.", ), "base_unk_x06_x07": MapFieldInfo( "Unknown Base [06-07]", int, -1, "Unknown base data integer.", ), "base_unk_x07_x08": MapFieldInfo( "Unknown Base [07-08]", int, -1, "Unknown base data integer.", ), "lantern_id": MapFieldInfo( "Lantern ID", int, 0, "Lantern param ID.", ), "lod_id": MapFieldInfo( "LoD ID", int, -1, "LoD (level of detail) param ID.", ), "base_unk_x0e_x0f": MapFieldInfo( "Unknown Base [0e-0f]", int, 20, "Unknown base data integer.", ), "base_unk_x0f_x10": MapFieldInfo( "Unknown Base [0f-10]", int, 0, "Unknown base data integer.", ), } FIELD_ORDER = ( "base_unk_x04_x05", "base_unk_x05_x06", "base_unk_x06_x07", "base_unk_x07_x08", "lantern_id", "lod_id", "base_unk_x0e_x0f", "base_unk_x0f_x10", ) sib_path: str base_unk_x04_x05: int base_unk_x05_x06: int base_unk_x06_x07: int base_unk_x07_x08: int lantern_id: int lod_id: int base_unk_x0e_x0f: int base_unk_x0f_x10: int def __init__(self, source=None, **kwargs): self._instance_index = 0 super().__init__(source, **kwargs) def unpack(self, msb_reader: BinaryReader): part_offset = msb_reader.position header = msb_reader.unpack_struct(self.PART_HEADER_STRUCT) if header["__part_type"] != self.ENTRY_SUBTYPE: raise ValueError(f"Unexpected part type enum {header['part_type']} for class {self.__class__.__name__}.") self._instance_index = header["_instance_index"] self._model_index = header["_model_index"] self._part_type_index = header["_part_type_index"] for transform in ("translate", "rotate", "scale"): setattr(self, transform, Vector3(header[transform])) self._draw_groups = int_group_to_bit_set(header["__draw_groups"], assert_size=8) self._display_groups = int_group_to_bit_set(header["__display_groups"], assert_size=8) self._backread_groups = int_group_to_bit_set(header["__backread_groups"], assert_size=8) self.description = msb_reader.unpack_string( offset=part_offset + header["__description_offset"], encoding="utf-16-le", ) self.name = msb_reader.unpack_string( offset=part_offset + header["__name_offset"], encoding="utf-16-le", ) self.sib_path = msb_reader.unpack_string( offset=part_offset + header["__sib_path_offset"], encoding="utf-16-le", ) msb_reader.seek(part_offset + header["__base_data_offset"]) base_data = msb_reader.unpack_struct(self.PART_BASE_DATA_STRUCT) self.set(**base_data) msb_reader.seek(part_offset + header["__type_data_offset"]) self.unpack_type_data(msb_reader) self._unpack_gparam_data(msb_reader, part_offset, header) self._unpack_scene_gparam_data(msb_reader, part_offset, header) def pack(self) -> bytes: """Pack to bytes, presumably as part of a full `MSB` pack.""" # Validate draw/display/backread groups before doing any real work. draw_groups = bit_set_to_int_group(self._draw_groups, group_size=8) display_groups = bit_set_to_int_group(self._display_groups, group_size=8) backread_groups = bit_set_to_int_group(self._backread_groups, group_size=8) description_offset = self.PART_HEADER_STRUCT.size packed_description = self.description.encode("utf-16-le") + b"\0\0" name_offset = description_offset + len(packed_description) packed_name = self.get_name_to_pack().encode("utf-16-le") + b"\0\0" sib_path_offset = name_offset + len(packed_name) packed_sib_path = self.sib_path.encode("utf-16-le") + b"\0\0" if self.sib_path else b"\0\0" strings_size = len(packed_description + packed_name + packed_sib_path) if strings_size <= 0x38: packed_sib_path += b"\0" * (0x3c - strings_size) else: packed_sib_path += b"\0" * 8 while len(packed_description + packed_name + packed_sib_path) % 4 != 0: packed_sib_path += b"\0" # Not done in SoulsFormats, but makes sense to me. base_data_offset = sib_path_offset + len(packed_sib_path) packed_base_data = self.PART_BASE_DATA_STRUCT.pack(self) type_data_offset = base_data_offset + len(packed_base_data) packed_type_data = self.pack_type_data() gparam_data_offset = type_data_offset + len(packed_type_data) packed_gparam_data = self._pack_gparam_data() scene_gparam_data_offset = gparam_data_offset + len(packed_gparam_data) packed_scene_gparam_data = self._pack_scene_gparam_data() try: packed_header = self.PART_HEADER_STRUCT.pack( __description_offset=description_offset, __name_offset=name_offset, _instance_index=self._instance_index, __part_type=self.ENTRY_SUBTYPE, _part_type_index=self._part_type_index, _model_index=self._model_index, __sib_path_offset=sib_path_offset, translate=list(self.translate), rotate=list(self.rotate), scale=list(self.scale), __draw_groups=draw_groups, __display_groups=display_groups, __backread_groups=backread_groups, __base_data_offset=base_data_offset, __type_data_offset=type_data_offset, __gparam_data_offset=gparam_data_offset, __scene_gparam_data_offset=scene_gparam_data_offset, ) except struct.error: raise MapError(f"Could not pack header data of MSB part '{self.name}'. See traceback.") return ( packed_header + packed_description + packed_name + packed_sib_path + packed_base_data + packed_type_data + packed_gparam_data + packed_scene_gparam_data ) def _unpack_gparam_data(self, msb_reader: BinaryReader, part_offset, header): pass def _pack_gparam_data(self): return b"" def _unpack_scene_gparam_data(self, msb_reader: BinaryReader, part_offset, header): pass def _pack_scene_gparam_data(self): return b""
def scale(self, value): self._scale = Vector3(value)
def __init__(self, source=None, **kwargs): self._scale = Vector3().ones() super().__init__(source, **kwargs)
class MSBRegion(MSBEntryEntityCoordinates, abc.ABC): ENTRY_SUBTYPE: MSBRegionSubtype = None REGION_STRUCT: BinaryStruct = None REGION_TYPE_DATA_STRUCT: BinaryStruct = None NAME_ENCODING = "" UNKNOWN_DATA_SIZE = -1 FIELD_INFO = MSBEntryEntityCoordinates.FIELD_INFO | { "translate": MapFieldInfo( "Translate", Vector3, Vector3.zero(), "3D coordinates of the region's position. Note that this is the middle of the bottom face for box " "regions.", ), "rotate": MapFieldInfo( "Rotate", Vector3, Vector3.zero(), "Euler angles for region rotation around its local X, Y, and Z axes.", ), } translate: Vector3 rotate: Vector3 def __init__(self, source=None, **kwargs): self._region_index = None # Final automatic assignment done on `MSB.pack()`. super().__init__(source=source, **kwargs) def unpack(self, msb_buffer): region_offset = msb_buffer.tell() base_data = self.REGION_STRUCT.unpack(msb_buffer) self.name = read_chars_from_buffer( msb_buffer, offset=region_offset + base_data["name_offset"], encoding=self.NAME_ENCODING, ) self._region_index = base_data["__region_index"] self.translate = Vector3(base_data["translate"]) self.rotate = Vector3(base_data["rotate"]) self.check_null_field(msb_buffer, region_offset + base_data["unknown_offset_1"]) self.check_null_field(msb_buffer, region_offset + base_data["unknown_offset_2"]) if base_data["type_data_offset"] != 0: msb_buffer.seek(region_offset + base_data["type_data_offset"]) self.unpack_type_data(msb_buffer) msb_buffer.seek(region_offset + base_data["entity_id_offset"]) self.entity_id = struct.unpack("i", msb_buffer.read(4))[0] return region_offset + base_data["entity_id_offset"] def pack(self, region_index=0): name_offset = self.REGION_STRUCT.size packed_name = pad_chars(self.get_name_to_pack(), encoding=self.NAME_ENCODING, pad_to_multiple_of=4) unknown_offset_1 = name_offset + len(packed_name) unknown_offset_2 = unknown_offset_1 + 4 packed_type_data = self.pack_type_data() if packed_type_data: type_data_offset = unknown_offset_2 + 4 entity_id_offset = type_data_offset + len(packed_type_data) else: type_data_offset = 0 entity_id_offset = unknown_offset_2 + 4 packed_base_data = self.REGION_STRUCT.pack( name_offset=name_offset, __region_index=region_index, region_type=self.ENTRY_SUBTYPE, translate=list(self.translate), rotate=list(self.rotate), unknown_offset_1=unknown_offset_1, unknown_offset_2=unknown_offset_2, type_data_offset=type_data_offset, entity_id_offset=entity_id_offset, ) packed_entity_id = struct.pack("i", self.entity_id) return packed_base_data + packed_name + b"\0\0\0\0" * 2 + packed_type_data + packed_entity_id def unpack_type_data(self, msb_buffer): self.set(**self.REGION_TYPE_DATA_STRUCT.unpack(msb_buffer)) def pack_type_data(self): return self.REGION_TYPE_DATA_STRUCT.pack(self) def set_indices(self, region_index): self._region_index = region_index @classmethod def check_null_field(cls, msb_buffer, offset_to_null): msb_buffer.seek(offset_to_null) zero = msb_buffer.read(cls.UNKNOWN_DATA_SIZE) if zero != b"\0" * cls.UNKNOWN_DATA_SIZE: _LOGGER.warning( f"Null data entry in `{cls.__name__}` was not zero: {zero}.")
def read(self, buffer: io.BufferedIOBase, layout: BufferLayout, uv_factor: float): self.uvs = [] self.tangents = [] self.colors = [] for member in layout: not_implemented = False if member.semantic == LayoutSemantic.Position: if member.layout_type == LayoutType.Float3: self.position = Vector3(unpack_from_buffer(buffer, "<3f")) elif member.layout_type == LayoutType.Float4: self.position = Vector3(unpack_from_buffer(buffer, "<3f"))[:3] elif member.layout_type == LayoutType.EdgeCompressed: raise NotImplementedError( "Soulstruct cannot load FLVERs with edge-compressed vertex positions." ) else: not_implemented = True elif member.semantic == LayoutSemantic.BoneWeights: if member.layout_type == LayoutType.Byte4A: self.bone_weights = VertexBoneWeights( * [w / 127.0 for w in unpack_from_buffer(buffer, "<4b")]) elif member.layout_type == LayoutType.Byte4C: self.bone_weights = VertexBoneWeights( * [w / 255.0 for w in unpack_from_buffer(buffer, "<4B")]) elif member.layout_type == LayoutType.UVPair: self.bone_weights = VertexBoneWeights(*[ w / 32767.0 for w in unpack_from_buffer(buffer, "<4h") ]) elif member.layout_type == LayoutType.Short4ToFloat4A: self.bone_weights = VertexBoneWeights(*[ w / 32767.0 for w in unpack_from_buffer(buffer, "<4h") ]) else: not_implemented = True elif member.semantic == LayoutSemantic.BoneIndices: if member.layout_type == LayoutType.Byte4B: self.bone_indices = VertexBoneIndices( *unpack_from_buffer(buffer, "<4B")) elif member.layout_type == LayoutType.ShortBoneIndices: self.bone_indices = VertexBoneIndices( *unpack_from_buffer(buffer, "<4h")) elif member.layout_type == LayoutType.Byte4E: self.bone_indices = VertexBoneIndices( *unpack_from_buffer(buffer, "<4B")) else: not_implemented = True elif member.semantic == LayoutSemantic.Normal: if member.layout_type == LayoutType.Float3: self.normal = Vector3(unpack_from_buffer(buffer, "<3f")) elif member.layout_type == LayoutType.Float4: self.normal = Vector3(unpack_from_buffer(buffer, "<3f")) float_normal_w = unpack_from_buffer(buffer, "<f")[0] self.normal_w = int(float_normal_w) if self.normal_w != float_normal_w: raise ValueError( f"`normal_w` float was not a whole number.") elif member.layout_type in { LayoutType.Byte4A, LayoutType.Byte4B, LayoutType.Byte4C, LayoutType.Byte4E }: self.normal = Vector3([ (x - 127) / 127.0 for x in unpack_from_buffer(buffer, "<3B") ]) self.normal_w = unpack_from_buffer(buffer, "<B")[0] elif member.layout_type == LayoutType.Short2toFloat2: self.normal_w = unpack_from_buffer(buffer, "<B")[0] self.normal = Vector3( [x / 127.0 for x in unpack_from_buffer(buffer, "<3b")]) elif member.layout_type == LayoutType.Short4ToFloat4A: self.normal = Vector3([ x / 32767.0 for x in unpack_from_buffer(buffer, "<3h") ]) self.normal_w = unpack_from_buffer(buffer, "<h")[0] elif member.layout_type == LayoutType.Short4ToFloat4B: self.normal = Vector3([ (x - 32767) / 32767.0 for x in unpack_from_buffer(buffer, "<3H") ]) self.normal_w = unpack_from_buffer(buffer, "<h")[0] else: not_implemented = True elif member.semantic == LayoutSemantic.UV: if member.layout_type == LayoutType.Float2: self.uvs.append( Vector3(*unpack_from_buffer(buffer, "<2f"), 0.0)) elif member.layout_type == LayoutType.Float3: self.uvs.append( Vector3(*unpack_from_buffer(buffer, "<3f"))) elif member.layout_type == LayoutType.Float4: self.uvs.append( Vector3(*unpack_from_buffer(buffer, "<2f"), 0.0)) self.uvs.append( Vector3(*unpack_from_buffer(buffer, "<2f"), 0.0)) elif member.layout_type in { LayoutType.Byte4A, LayoutType.Byte4B, LayoutType.Short2toFloat2, LayoutType.Byte4C, LayoutType.UV }: self.uvs.append( Vector3(*unpack_from_buffer(buffer, "<2h"), 0) / uv_factor) elif member.layout_type == LayoutType.UVPair: self.uvs.append( Vector3(*unpack_from_buffer(buffer, "<2h"), 0) / uv_factor) self.uvs.append( Vector3(*unpack_from_buffer(buffer, "<2h"), 0) / uv_factor) elif member.layout_type == LayoutType.Short4ToFloat4B: self.uvs.append( Vector3(*unpack_from_buffer(buffer, "<3h")) / uv_factor) if unpack_from_buffer(buffer, "<h") != 0: raise ValueError( "Expected null byte after reading UV | Short4ToFloat4B vertex member." ) else: not_implemented = True elif member.semantic == LayoutSemantic.Tangent: if member.layout_type == LayoutType.Float4: self.tangents.append( Vector4(*unpack_from_buffer(buffer, "<4f"))) elif member.layout_type in { LayoutType.Byte4A, LayoutType.Byte4B, LayoutType.Byte4C, LayoutType.Short4ToFloat4A, LayoutType.Byte4E, }: tangent = Vector4([ (x - 127) / 127.0 for x in unpack_from_buffer(buffer, "<4B") ]) self.tangents.append(tangent) else: not_implemented = True elif member.semantic == LayoutSemantic.Bitangent: if member.layout_type in { LayoutType.Byte4A, LayoutType.Byte4B, LayoutType.Byte4C, LayoutType.Byte4E }: self.bitangent = Vector4([ (x - 127) / 127.0 for x in unpack_from_buffer(buffer, "<4B") ]) else: not_implemented = True elif member.semantic == LayoutSemantic.VertexColor: if member.layout_type == LayoutType.Float4: self.colors.append( Color(*unpack_from_buffer(buffer, "<4f"))) elif member.layout_type in { LayoutType.Byte4A, LayoutType.Byte4C }: self.colors.append( Color(*[ b / 255.0 for b in unpack_from_buffer(buffer, "<4B") ])) else: not_implemented = True else: not_implemented = True if not_implemented: raise NotImplementedError( f"Unsupported vertex member semantic/type combination: " f"{member.semantic.name} | {member.layout_type.name}")
def read(self, reader: BinaryReader, layout: BufferLayout, uv_factor: float): self.uvs = [] self.tangents = [] self.colors = [] with reader.temp_offset(reader.position): self.raw = reader.read(layout.get_total_size()) for member in layout: not_implemented = False if member.semantic == LayoutSemantic.Position: if member.layout_type == LayoutType.Float3: self.position = Vector3(reader.unpack("<3f")) elif member.layout_type == LayoutType.Float4: self.position = Vector3(reader.unpack("<3f"))[:3] elif member.layout_type == LayoutType.EdgeCompressed: raise NotImplementedError( "Soulstruct cannot load FLVERs with edge-compressed vertex positions." ) else: not_implemented = True elif member.semantic == LayoutSemantic.BoneWeights: if member.layout_type == LayoutType.Byte4A: self.bone_weights = VertexBoneWeights( *[w / 127.0 for w in reader.unpack("<4b")]) elif member.layout_type == LayoutType.Byte4C: self.bone_weights = VertexBoneWeights( *[w / 255.0 for w in reader.unpack("<4B")]) elif member.layout_type in { LayoutType.UVPair, LayoutType.Short4ToFloat4A }: self.bone_weights = VertexBoneWeights( *[w / 32767.0 for w in reader.unpack("<4h")]) else: not_implemented = True elif member.semantic == LayoutSemantic.BoneIndices: if member.layout_type in { LayoutType.Byte4B, LayoutType.Byte4E }: self.bone_indices = VertexBoneIndices( *reader.unpack("<4B")) elif member.layout_type == LayoutType.ShortBoneIndices: self.bone_indices = VertexBoneIndices( *reader.unpack("<4h")) else: not_implemented = True elif member.semantic == LayoutSemantic.Normal: if member.layout_type == LayoutType.Float3: self.normal = Vector3(reader.unpack("<3f")) elif member.layout_type == LayoutType.Float4: self.normal = Vector3(reader.unpack("<3f")) float_normal_w = reader.unpack_value("<f") self.normal_w = int(float_normal_w) if self.normal_w != float_normal_w: raise ValueError( f"`normal_w` float was not a whole number.") elif member.layout_type in { LayoutType.Byte4A, LayoutType.Byte4B, LayoutType.Byte4C, LayoutType.Byte4E }: self.normal = Vector3([(x - 127) / 127.0 for x in reader.unpack("<3B")]) self.normal_w = reader.unpack_value("<B") elif member.layout_type == LayoutType.Short2toFloat2: self.normal_w = reader.unpack_value("<B") self.normal = Vector3( [x / 127.0 for x in reader.unpack("<3b")]) elif member.layout_type == LayoutType.Short4ToFloat4A: self.normal = Vector3( [x / 32767.0 for x in reader.unpack("<3h")]) self.normal_w = reader.unpack_value("<h") elif member.layout_type == LayoutType.Short4ToFloat4B: self.normal = Vector3([(x - 32767) / 32767.0 for x in reader.unpack("<3H")]) self.normal_w = reader.unpack_value("<h") else: not_implemented = True elif member.semantic == LayoutSemantic.UV: if member.layout_type == LayoutType.Float2: self.uvs.append(Vector3(*reader.unpack("<2f"), 0.0)) elif member.layout_type == LayoutType.Float3: self.uvs.append(Vector3(*reader.unpack("<3f"))) elif member.layout_type == LayoutType.Float4: self.uvs.append(Vector3(*reader.unpack("<2f"), 0.0)) self.uvs.append(Vector3(*reader.unpack("<2f"), 0.0)) elif member.layout_type in { LayoutType.Byte4A, LayoutType.Byte4B, LayoutType.Short2toFloat2, LayoutType.Byte4C, LayoutType.UV }: self.uvs.append( Vector3(*reader.unpack("<2h"), 0) / uv_factor) elif member.layout_type == LayoutType.UVPair: self.uvs.append( Vector3(*reader.unpack("<2h"), 0) / uv_factor) self.uvs.append( Vector3(*reader.unpack("<2h"), 0) / uv_factor) elif member.layout_type == LayoutType.Short4ToFloat4B: self.uvs.append(Vector3(*reader.unpack("<3h")) / uv_factor) if reader.unpack_value("<h") != 0: raise ValueError( "Expected zero short after reading UV | Short4ToFloat4B vertex member." ) else: not_implemented = True elif member.semantic == LayoutSemantic.Tangent: if member.layout_type == LayoutType.Float4: self.tangents.append(Vector4(*reader.unpack("<4f"))) elif member.layout_type in { LayoutType.Byte4A, LayoutType.Byte4B, LayoutType.Byte4C, LayoutType.Short4ToFloat4A, LayoutType.Byte4E, }: tangent = Vector4([(x - 127) / 127.0 for x in reader.unpack("<4B")]) self.tangents.append(tangent) else: not_implemented = True elif member.semantic == LayoutSemantic.Bitangent: if member.layout_type in { LayoutType.Byte4A, LayoutType.Byte4B, LayoutType.Byte4C, LayoutType.Byte4E }: self.bitangent = Vector4([(x - 127) / 127.0 for x in reader.unpack("<4B")]) else: not_implemented = True elif member.semantic == LayoutSemantic.VertexColor: if member.layout_type == LayoutType.Float4: self.colors.append(ColorRGBA(*reader.unpack("<4f"))) elif member.layout_type in { LayoutType.Byte4A, LayoutType.Byte4C }: # Convert byte channnels [0-255] to float channels [0-1]. self.colors.append( ColorRGBA(*[b / 255.0 for b in reader.unpack("<4B")])) else: not_implemented = True else: not_implemented = True if not_implemented: raise NotImplementedError( f"Unsupported vertex member semantic/type combination: " f"{member.semantic.name} | {member.layout_type.name}")