예제 #1
0
 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)
예제 #2
0
    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]
예제 #3
0
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)
예제 #4
0
파일: maps.py 프로젝트: Grimrukh/soulstruct
    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()
예제 #5
0
    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()
예제 #6
0
 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]
예제 #7
0
    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]
예제 #8
0
    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)
예제 #9
0
파일: msb.py 프로젝트: wrekklol/soulstruct
    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)
예제 #10
0
파일: core.py 프로젝트: LugeBox/soulstruct
    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)
예제 #11
0
파일: node.py 프로젝트: LugeBox/soulstruct
 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)
예제 #12
0
파일: bone.py 프로젝트: Grimrukh/soulstruct
 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
예제 #13
0
    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
예제 #14
0
    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}")
예제 #15
0
    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"]
예제 #16
0
파일: maps.py 프로젝트: wrekklol/soulstruct
 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()
예제 #17
0
    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"]
예제 #18
0
    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)
예제 #19
0
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)
예제 #20
0
 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)
예제 #21
0
 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()
예제 #22
0
 def rotate(self, value):
     if isinstance(value, (int, float)):
         self._rotate = Vector3(0, value, 0)
     else:
         self._rotate = Vector3(value)
예제 #23
0
 def translate(self, value):
     self._translate = Vector3(value)
예제 #24
0
 def __init__(self, source=None, **kwargs):
     self._translate = Vector3.zero()
     self._rotate = Vector3.zero()
     super().__init__(source=source, **kwargs)
예제 #25
0
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""
예제 #26
0
 def scale(self, value):
     self._scale = Vector3(value)
예제 #27
0
 def __init__(self, source=None, **kwargs):
     self._scale = Vector3().ones()
     super().__init__(source, **kwargs)
예제 #28
0
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}.")
예제 #29
0
    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}")
예제 #30
0
    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}")