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]
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)
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 __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 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
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 __init__(self, source=None, **kwargs): self._translate = Vector3.zero() self._rotate = Vector3.zero() super().__init__(source=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}.")
class MSBWindEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.Wind EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("wind_vector_min", "3f"), ("unk_x04_x08", "f"), ("wind_vector_max", "3f"), ("unk_x0c_x10", "f"), ("wind_swing_cycles", "4f"), ("wind_swing_powers", "4f"), ) FIELD_INFO = MSBEvent.FIELD_INFO | { "wind_vector_min": MapFieldInfo( "Wind Vector Min", Vector3, Vector3.zero(), "Wind vector minimum.", ), "unk_x04_x08": MapFieldInfo( "Unknown [04-08]", float, 0.0, "Unknown Wind parameter (floating-point number).", ), "wind_vector_max": MapFieldInfo( "Wind Vector Max", Vector3, Vector3.zero(), "Wind vector maximum.", ), "unk_x0c_x10": MapFieldInfo( "Unknown [0c-10]", float, 0.0, "Unknown Wind parameter (floating-point number).", ), "wind_swing_cycles": MapFieldInfo( "Wind Swing Cycles", list, [0.0, 0.0, 0.0, 0.0], "Wind swing cycles (four values).", ), "wind_swing_powers": MapFieldInfo( "Wind Swing Powers", list, [0.0, 0.0, 0.0, 0.0], "Wind swing powers (four values).", ), } FIELD_ORDER = ( "wind_vector_min", "unk_x04_x08", "wind_vector_max", "unk_x0c_x10", "wind_swing_cycles", "wind_swing_powers", ) unk_x04_x08: float unk_x0c_x10: float 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) @property def wind_vector_min(self): return self._wind_vector_min @wind_vector_min.setter def wind_vector_min(self, value): self._wind_vector_min = Vector3(value) @property def wind_vector_max(self): return self._wind_vector_max @wind_vector_max.setter def wind_vector_max(self, value): self._wind_vector_max = Vector3(value) @property def wind_swing_cycles(self): return self._wind_swing_cycles @wind_swing_cycles.setter def wind_swing_cycles(self, value): try: value = list(value) if not len(value) == 4 or not all( isinstance(v, (int, float)) for v in value): raise ValueError except (TypeError, ValueError): raise ValueError( f"`wind_swing_cycles` must be a sequence of four numbers.") self._wind_swing_cycles = value @property def wind_swing_powers(self): return self._wind_swing_powers @wind_swing_powers.setter def wind_swing_powers(self, value): try: value = list(value) if not len(value) == 4 or not all( isinstance(v, (int, float)) for v in value): raise ValueError except (TypeError, ValueError): raise ValueError( f"`wind_swing_powers` must be a sequence of four numbers.") self._wind_swing_powers = value
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=None, ): """Rotate and then translate entire map so that an entity with a translate of `start_translate` and rotate of `start_rotate` ends up with a translate of `end_translate` and a rotate of `end_rotate`. Optionally, move only a subset of entry names given in `selected_entry_names`. """ if selected_entries is not None: selected_entries = list( selected_entries ) # so strings can be replaced with part instances for i, entry in enumerate(selected_entries): if isinstance(entry, str): try: selected_part = self.parts.get_entry_by_name(entry) except KeyError: selected_part = None try: selected_region = self.regions.get_entry_by_name(entry) except KeyError: selected_region = None if selected_part and selected_region: raise ValueError( f"Found both a Part and Region with name '{entry}'. You must pass the desired entry " f"instance directly.") elif selected_part: selected_entries[i] = selected_part elif selected_region: selected_entries[i] = selected_region else: raise ValueError( f"Could not find a Part or Region named '{entry}' in MSB." ) elif not isinstance(entry, MSBEntry): raise TypeError( "`selected_entries` should contain only `MSBEntry` instances or Part names." ) if start_translate is None: start_translate = Vector3.zero() elif not isinstance(start_translate, Vector3): start_translate = Vector3(start_translate) if end_translate is None: end_translate = Vector3.zero() elif not isinstance(end_translate, Vector3): end_translate = Vector3(end_translate) if start_rotate is None: start_rotate = Vector3.zero() elif isinstance(start_rotate, (int, float)): start_rotate = Vector3(0, start_rotate, 0) else: start_rotate = Vector3(start_rotate) if end_rotate is None: end_rotate = Vector3.zero() elif isinstance(end_rotate, (int, float)): end_rotate = Vector3(0, end_rotate, 0) else: end_rotate = 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_rotate = m_end_rotate @ m_start_rotate.T # Apply global rotation to start point to determine required global translation. translation = end_translate - (m_world_rotate @ start_translate ) # type: Vector3 self.rotate_all_in_world(m_world_rotate, selected_entries=selected_entries) self.translate_all(translation, selected_entries=selected_entries)