def rgb_to_palette(img: Image, palette: bytes, name: str) -> bytes: """Convert an RGB image to palette entries. Pillow's quantize is pretty broken, so don't use that. :param img: The image to be converted. Must be RGB. :param palette: The colors of the palette. Must be RGB. :param name: The name of the image (for debug). :return: The corresponding indices in the palette for each pixel. :raises Mech3TextureError: if the image is not RGB :raises Mech3TextureError: if the palette contains duplicate colors :raises Mech3TextureError: if the palette does not contain a color in the image """ assert_eq("image mode", "RGB", img.mode, name, Mech3TextureError) rgb_to_index = {} it = iter(palette) for i, rgb in enumerate(zip(it, it, it)): if rgb in rgb_to_index: # this occurs frequently; but the original images use the first color/index LOG.debug("Duplicate color found in palette of %s", name) else: rgb_to_index[rgb] = i size = img.width * img.height buf = bytearray(size) it = iter(img.tobytes()) try: for i, rgb in enumerate(zip(it, it, it)): buf[i] = rgb_to_index[rgb] except KeyError: # pragma: no cover raise Mech3TextureError(f"Color not found in palette of {name}") return bytes(buf)
def validate_length(cls, reader: BinReader, actual_length: int) -> None: assert_eq( f"{cls._NAME} size", cls._STRUCT.size, actual_length, reader.prev + 4, )
def read_activation_prereq_anim(reader: BinReader) -> str: name_raw, zero32, zero36 = reader.read(ACTIV_PREREQ_ANIM) with assert_ascii("activ prereq name", name_raw, reader.prev + 0): name = ascii_zterm_padded(name_raw) # field offset from start of record assert_eq("activ prereq field 40", 0, zero32, reader.prev + 32) assert_eq("activ prereq field 44", 0, zero36, reader.prev + 36) return name
def read(cls, reader: BinReader, _anim_def: AnimDef) -> InvalidateAnimation: name_raw, sentinel = reader.read(cls._STRUCT) with assert_ascii("name", name_raw, reader.prev + 0): name = ascii_zterm_padded(name_raw) assert_eq("sentinel", 0, sentinel, reader.prev + 0) return cls(name=name)
def read(cls, reader: BinReader, anim_def: AnimDef) -> ObjectCycleTexture: one, zero, node_index, reset = reader.read(cls._STRUCT) # increment? assert_eq("field 0", 1, one, reader.prev + 0) # start index? assert_eq("field 4", 0, zero, reader.prev + 2) node = anim_def.get_node(node_index - 1, reader.prev + 4) assert_between("reset", 0, 5, reset, reader.prev + 6) return cls(node=node, reset=reset)
def read_anim_def_zero(reader: BinReader) -> None: # the first entry is always zero data = bytearray(reader.read_bytes(ANIM_DEF.size)) # except for this one byte? assert_eq("anim def header byte 153", 3, data[153], reader.prev + 153) data[153] = 0 assert_all_zero("anim def header", data, reader.prev) bdata = reader.read_bytes(RESET_STATE.size) assert_all_zero("anim def reset", bdata, reader.prev)
def validate_and_read(cls, reader: BinReader, _anim_def: AnimDef, actual_length: int) -> ObjectMotionSIScript: abs_end = reader.offset + actual_length ( index, count, zero08, zero12, zero16, zero20, ) = reader.read(cls._STRUCT) assert_between("index", 0, 20, index, reader.prev + 0) # 12 assert_eq("field 08", 0.0, zero08, reader.prev + 8) # 20 assert_eq("field 12", 0.0, zero12, reader.prev + 12) # 24 assert_eq("field 16", 0, zero16, reader.prev + 16) # 28 assert_eq("field 20", 0, zero20, reader.prev + 20) # 32 frames = [] for i in range(count): flag, start_time, end_time = reader.read(SI_SCRIPT_FRAME) assert_between(f"motion {i} flag", 1, 7, flag, reader.prev + 0) assert_ge(f"motion {i} start", 0.0, start_time, reader.prev + 4) if end_time != 0.0: assert_ge(f"motion {i} end", start_time, end_time, reader.prev + 8) if (flag & 1) == 0: translate: Optional[Vector] = None else: values = cast(Values, reader.read(SI_SCRIPT_DATA)) translate = parse_vec_data(values, start_time) if (flag & 2) == 0: rotate: Optional[Quaternion] = None else: values = cast(Values, reader.read(SI_SCRIPT_DATA)) rotate = parse_quat_data(values, start_time) if (flag & 4) == 0: scale: Optional[Vector] = None else: values = cast(Values, reader.read(SI_SCRIPT_DATA)) scale = parse_vec_data(values, start_time) frame = MotionFrame( start=start_time, end=end_time, translate=translate, rotate=rotate, scale=scale, ) frames.append(frame) assert_eq("motion script end", abs_end, reader.offset, reader.offset) return cls(index=index, frames=frames)
def read(cls, reader: BinReader, anim_def: AnimDef) -> ObjectTranslateState: at_node_matrix, tx, ty, tz, node_index = reader.read(cls._STRUCT) assert_eq("field 00", 0, at_node_matrix, reader.prev + 0) if node_index < 1: assert_lt("node index", -13107190, node_index, reader.prev + 16) node = "INPUT_NODE" else: node = anim_def.get_node(node_index - 1, reader.prev + 16) return cls(node=node, state=(tx, ty, tz))
def _read_static_sounds(reader: BinReader, count: int) -> List[NameRaw]: # the first entry is always zero name_raw, ptr = reader.read(STATIC_SOUND) assert_all_zero("name", name_raw, reader.prev + 0) assert_eq("field 32", 0, ptr, reader.prev + 32) sounds = [] for _ in range(1, count): name_raw, ptr = reader.read(STATIC_SOUND) with assert_ascii("name", name_raw, reader.prev + 0): name, pad = ascii_zterm_partition(name_raw) assert_eq("field 32", 0, ptr, reader.prev + 32) sounds.append(NameRaw(name=name, pad=Base64(pad))) return sounds
def _read_anim_header(reader: BinReader) -> List[AnimName]: (signature, version, count) = reader.read(ANIM_FILE_HEADER) LOG.debug( "Anim signature 0x%08x, version %d", signature, version, ) assert_eq("signature", SIGNATURE, signature, reader.prev + 0) assert_eq("version", VERSION, version, reader.prev + 4) LOG.debug("Reading %d anim names at %d", count, reader.offset) names = [] for _ in range(count): name_raw, unk = reader.read(ANIM_NAME) with assert_ascii("name", name_raw, reader.prev + 0): name, pad = ascii_zterm_partition(name_raw) names.append(AnimName(name=name, pad=Base64(pad), unk=unk)) return names
def _read_mesh_zero(reader: BinReader, mesh_count: int, array_size: int) -> None: LOG.debug("Reading %d zeroed meshes at %d", array_size - mesh_count, reader.offset) for i in range(mesh_count, array_size): mesh = reader.read(MESH) for j, value in enumerate(mesh): offset = j * 4 assert_eq(f"field {offset:02d}", 0, value, reader.prev + offset) expected = i + 1 if expected == array_size: expected = -1 (index, ) = reader.read(SINT32) assert_eq("index", expected, index, reader.prev)
def read(cls, reader: BinReader, anim_def: AnimDef) -> ObjectOpacityState: is_set_raw, state_raw, opacity, node_index = reader.read(cls._STRUCT) # this could be another value (e.g. -1), in which case is set is not changed assert_in("is set", (0, 1), is_set_raw, reader.prev + 0) # the state does not seem to depend on is_set? assert_in("state", (0, 1), state_raw, reader.prev + 2) state = state_raw == 1 if state: assert_between("opacity", 0.0, 1.0, opacity, reader.prev + 4) else: assert_eq("opacity", 0.0, opacity, reader.prev + 4) node = anim_def.get_node(node_index - 1, reader.prev + 8) return cls(node=node, is_set=is_set_raw == 1, state=state, opacity=opacity)
def _read_anim_refs(reader: BinReader, count: int) -> List[NameRaw]: # the first entry... is not zero! as this is not a node list # there's one anim ref per CALL_ANIMATION, and there may be duplicates to # the same anim since multiple calls might need to be ordered anim_refs = [] for _ in range(count): name_raw, zero64, zero68 = reader.read(ANIM_REF) # a bunch of these values are properly zero-terminated at 32 and beyond, # but not all... i suspect a lack of memset with assert_ascii("name", name_raw, reader.prev + 0): name, pad = ascii_zterm_partition(name_raw) assert_eq("field 64", 0, zero64, reader.prev + 64) assert_eq("field 68", 0, zero68, reader.prev + 68) anim_refs.append(NameRaw(name=name, pad=Base64(pad.rstrip(b"\0")))) return anim_refs
def _read_node_data_lod(reader: BinReader) -> LevelOfDetail: ( level, # 00 range_near_sq, # 04 range_far, # 08 range_far_sq, # 12 zero16, # 44 zero bytes unk60, # 60 unk64, # 64 one68, # 68 zero72, # 72 unk76, # 76 ) = reader.read(LEVEL_OF_DETAIL) assert_in("level", (0, 1), level, reader.prev + 0) assert_between( "range near sq", 0.0, 1000.0 * 1000.0, range_near_sq, reader.prev + 4, ) range_near = sqrt(range_near_sq) assert_ge("range far", 0.0, range_far, reader.prev + 8) expected = force_single_prec(range_far * range_far) assert_eq("range far sq", expected, range_far_sq, reader.prev + 12) assert_all_zero("field 16", zero16, reader.prev + 16) # TODO: assert_ge("field 60", 0.0, unk60, reader.prev + 60) expected = force_single_prec(unk60 * unk60) assert_eq("field 64", expected, unk64, reader.prev + 64) assert_eq("field 68", 1, one68, reader.prev + 68) # TODO: assert_in("field 72", (0, 1), zero72, reader.prev + 72) if zero72 == 0: assert_eq("field 76", 0, unk76, reader.prev + 76) else: assert_ne("field 76", 0, unk76, reader.prev + 76) # object 3d nodes always have an action priority of 6 return LevelOfDetail( type="LOD", level=level == 1, range=(range_near, range_far), unk60=unk60, unk76=unk76, )
def read_anim(data: bytes) -> Tuple[AnimMetadata, List[AnimDef]]: reader = BinReader(data) LOG.debug("Reading animation data...") anim_names = _read_anim_header(reader) anim_count, anim_ptr, world_ptr = _read_anim_info(reader) anim_defs, anim_def_ptrs = _read_anim_defs(reader, anim_count) assert_eq("anim end", len(data), reader.offset, reader.offset) LOG.debug("Read animation data") anim_md = AnimMetadata( anim_names=anim_names, anim_ptr=anim_ptr, world_ptr=world_ptr, anim_def_ptrs=anim_def_ptrs, ) return anim_md, anim_defs
def _read_dynamic_sounds(reader: BinReader, count: int) -> List[NamePtrFlag]: # the first entry is always zero name_raw, flag, ptr, zero = reader.read(READER_LOOKUP) assert_all_zero("name", name_raw, reader.prev + 0) assert_eq("field 32", 0, flag, reader.prev + 32) assert_eq("field 36", 0, ptr, reader.prev + 36) assert_eq("field 40", 0, zero, reader.prev + 40) sounds = [] for _ in range(1, count): name_raw, flag, ptr, zero = reader.read(READER_LOOKUP) with assert_ascii("name", name_raw, reader.prev + 0): name = ascii_zterm_node_name(name_raw) assert_eq("field 32", 0, flag, reader.prev + 32) assert_ne("field 36", 0, ptr, reader.prev + 36) assert_eq("field 40", 0, zero, reader.prev + 40) sounds.append(NamePtrFlag(name=name, ptr=ptr)) return sounds
def _read_objects(reader: BinReader, count: int) -> List[NameRaw]: # the first entry is always zero data = reader.read_bytes(OBJECT.size) assert_all_zero("object", data, reader.prev) objects = [] for _ in range(1, count): (name_raw, zero32, bin_dump) = reader.read(OBJECT) with assert_ascii("name", name_raw, reader.prev + 0): name = ascii_zterm_node_name(name_raw) assert_eq("field 32", 0, zero32, reader.prev + 32) # TODO: this is cheating, but i have no idea how to interpret this data # sometimes it's sensible, e.g. floats. other times, it seems like random # garbage. objects.append(NameRaw(name=name, pad=Base64(bin_dump.rstrip(b"\0")))) return objects
def _read_lights(reader: BinReader, count: int) -> List[NamePtrFlag]: # the first entry is always zero name_raw, flag, ptr, zero = reader.read(READER_LOOKUP) assert_all_zero("name", name_raw, reader.prev + 0) assert_eq("field 32", 0, flag, reader.prev + 32) assert_eq("field 36", 0, ptr, reader.prev + 36) assert_eq("field 40", 0, zero, reader.prev + 40) lights = [] for _ in range(1, count): name_raw, flag, ptr, zero = reader.read(READER_LOOKUP) with assert_ascii("name", name_raw, reader.prev + 0): name = ascii_zterm_node_name(name_raw) assert_eq("field 32", 0, flag, reader.prev + 32) assert_ne("field 36", 0, ptr, reader.prev + 36) # if this were non-zero, it would cause the light to be removed instead # of added (???) assert_eq("field 40", 0, zero, reader.prev + 40) lights.append(NamePtrFlag(name=name, ptr=ptr)) return lights
def _parse_script(reader: BinReader, anim_def: AnimDef, rel_end: int) -> List[ScriptItem]: LOG.debug("Reading script with length %d at %d", rel_end, reader.offset) abs_end = rel_end + reader.offset script = [] while reader.offset < abs_end: stype, start_offset, pad, size, start_time = reader.read(SCRIPT_HEADER) assert_in("type", OBJECT_REGISTRY_NUM.keys(), stype, reader.prev + 0) assert_in("start offset", (1, 2, 3), start_offset, reader.prev + 1) assert_eq("field 02", 0, pad, reader.prev + 2) start_offset = StartOffset(start_offset) if start_time == 0.0: assert_eq("start offset", StartOffset.Animation, start_offset, reader.prev + 1) start_offset = StartOffset.Unset actual_length = size - SCRIPT_HEADER.size base_model = OBJECT_REGISTRY_NUM[stype] obj = base_model.validate_and_read(reader, anim_def, actual_length) item = ScriptItem( name=base_model._NAME, # pylint: disable=protected-access item=obj, start_offset=start_offset, start_time=start_time, ) script.append(item) assert_eq("script end", abs_end, reader.offset, reader.offset) LOG.debug("Read script") return script
def read_textures(reader: BinReader, count: int) -> List[Texture]: textures = [] for _ in range(count): zero00, zero04, texture_raw, used, index, mone36 = reader.read( TEXTURE_INFO) # not sure. a pointer to the previous texture in the global array? or a # pointer to the texture? assert_eq("field 00", 0, zero00, reader.prev + 0) # a non-zero value here causes additional dynamic code to be called assert_eq("field 04", 0, zero04, reader.prev + 4) with assert_ascii("texture", texture_raw, reader.prev + 8): texture, suffix = _ascii_zterm_suffix(texture_raw) # 2 if the texture is used, 0 if the texture is unused # 1 or 3 if the texture is being processed (deallocated?) assert_eq("used", 2, used, reader.prev + 28) # stores the texture's index in the global texture array assert_eq("index", 0, index, reader.prev + 32) # not sure. a pointer to the next texture in the global array? or # something to do with mipmaps? assert_eq("field 36", -1, mone36, reader.prev + 36) textures.append(Texture(name=texture, suffix=suffix)) return textures
def _read_puffers(reader: BinReader, count: int) -> List[NamePtrFlag]: # the first entry is always zero name_raw, flag, ptr, zero = reader.read(READER_LOOKUP) assert_all_zero("name", name_raw, reader.prev + 0) assert_eq("field 32", 0, flag, reader.prev + 32) assert_eq("field 36", 0, ptr, reader.prev + 36) assert_eq("field 40", 0, zero, reader.prev + 40) puffers = [] for _ in range(1, count): name_raw, flag, ptr, zero = reader.read(READER_LOOKUP) with assert_ascii("name", name_raw, reader.prev + 0): name = ascii_zterm_padded(name_raw) assert_eq("field 32", 0, (flag & 0x00FFFFFF), reader.prev + 32) # TODO: what does this flag mean? # this is something the code does, but i'm not sure why # some of these values make decent floating point numbers flag = flag >> 24 assert_ne("field 36", 0, ptr, reader.prev + 36) assert_eq("field 40", 0, zero, reader.prev + 40) puffers.append(NamePtrFlag(name=name, ptr=ptr, flag=flag)) return puffers
def read_materials(reader: BinReader, texture_count: int) -> Tuple[int, List[Material]]: (array_size, mat_count, index_max, mat_unknown) = reader.read(MATERIAL_HEADER) assert_eq("index max", mat_count, index_max, reader.prev + 8) assert_eq("field 12", mat_count - 1, mat_unknown, reader.prev + 12) materials_and_cycle = _read_materials(reader, mat_count, texture_count) _read_materials_zero(reader, mat_count, array_size) materials = [] for material, cycle_info_ptr in materials_and_cycle: if cycle_info_ptr: ( unk00, unk04, zero08, unk12, cycle_count1, cycle_count2, data_ptr, ) = reader.read(CYCLE_HEADER) assert_in("field 00", (0, 1), unk00, reader.prev + 0) # field 04 assert_eq("field 08", 0, zero08, reader.prev + 8) assert_between("field 12", 2.0, 16.0, unk12, reader.prev + 12) assert_eq("cycle count", cycle_count1, cycle_count2, reader.prev + 20) assert_ne("field 24", 0, data_ptr, reader.prev + 24) cycle_textures = reader.read(Struct(f"<{cycle_count1}I")) for i, cycle_texture in enumerate(cycle_textures): # the texture should be in range assert_between( "texture", 0, texture_count - 1, cycle_texture, reader.prev + i * 4 ) material.cycle = Cycle( textures=list(cycle_textures), unk00=unk00 == 1, unk04=unk04, unk12=unk12, info_ptr=cycle_info_ptr, data_ptr=data_ptr, ) materials.append(material) return array_size, materials
def read_activation_prereq_obj( reader: BinReader, required: bool, prereq_type: int, prev: OptPrereqObj) -> Tuple[OptPrereqObj, OptPrereqObj]: active, name_raw, ptr = reader.read(ACTIV_PREREQ_OBJ) with assert_ascii("activ prereq name", name_raw, reader.prev + 4): name = ascii_zterm_padded(name_raw) assert_ne("activ prereq ptr", 0, ptr, reader.prev + 36) if prereq_type == 3: assert_eq("activ prereq active", 0, active, reader.prev + 0) # remember the current node as the previous one prev = PrereqObject(required=required, active=False, name=name, ptr=ptr) return prev, None assert_in("activ prereq active", (0, 1), active, reader.prev + 0) if prev: assert_eq("activ prereq required", prev.required, required, reader.prev + 0) parent_name = prev.name parent_ptr = prev.ptr else: parent_name = "" parent_ptr = 0 obj = PrereqObject( required=required, active=active == 1, name=name, ptr=ptr, parent_name=parent_name, parent_ptr=parent_ptr, ) # set the previous node to NULL return None, obj
def read(cls, reader: BinReader, anim_def: AnimDef) -> SoundNode: ( name_raw, one32, inherit_trans, active_state, node_index, tx, ty, tz, ) = reader.read(cls._STRUCT) with assert_ascii("name", name_raw, reader.prev + 0): name = ascii_zterm_padded(name_raw) # this would cause the sound node to be dynamically allocated assert_eq("field 32", 1, one32, reader.prev + 32) # 44 assert_in("active state", (0, 1), active_state, reader.prev + 40) # 52 # this is actually a bit field. # if it's 1, then the translation would be applied directly to the node # if it's 2 and AT_NODE is given, then the translation is applied relative # to the node assert_in("field 36", (0, 2), inherit_trans, reader.prev + 36) # 48 if inherit_trans == 0: at_node = None assert_eq("at node", 0, node_index, reader.prev + 44) assert_eq("at tx", 0.0, tx, reader.prev + 48) assert_eq("at ty", 0.0, ty, reader.prev + 52) assert_eq("at tz", 0.0, tz, reader.prev + 56) else: node = anim_def.get_node(node_index - 1, reader.prev + 44) at_node = AtNodeShort(node=node, tx=tx, ty=ty, tz=tz) return cls(name=name, active_state=active_state == 1, at_node=at_node)
def _read_reset_state( reader: BinReader, anim_def: AnimDef, length: int, ptr: int, offset: int, ) -> Optional[SeqDef]: reset_raw, reset_ptr, reset_len = reader.read(RESET_STATE) with assert_ascii("reset end", reset_raw, reader.prev + 0): reset_end = ascii_zterm_padded(reset_raw) # this is always "RESET_SEQUENCE" assert_eq("reset end", "RESET_SEQUENCE", reset_end, reader.prev + 0) assert_eq("reset ptr", ptr, reset_ptr, reader.prev + 56) assert_eq("reset len", length, reset_len, reader.prev + 60) if not length: assert_eq("reset ptr", 0, ptr, offset) return None assert_ne("reset ptr", 0, ptr, offset) script = _parse_script(reader, anim_def, length) return SeqDef(name="RESET_SEQUENCE", ptr=ptr, script=script)
def _read_nodes(reader: BinReader, count: int) -> List[NamePtrFlag]: # the first entry is always zero name_raw, zero, ptr = reader.read(NODE) assert_all_zero("name", name_raw, reader.prev + 0) assert_eq("field 32", 0, zero, reader.prev + 32) assert_eq("field 36", 0, ptr, reader.prev + 36) nodes = [] for _ in range(1, count): name_raw, zero, ptr = reader.read(NODE) with assert_ascii("name", name_raw, reader.prev + 0): name = ascii_zterm_node_name(name_raw) assert_eq("field 32", 0, zero, reader.prev + 32) assert_ne("field 36", 0, ptr, reader.prev + 36) nodes.append(NamePtrFlag(name=name, ptr=ptr)) return nodes
def read(cls, reader: BinReader, _anim_def: AnimDef) -> FogState: ( name_raw, flag_raw, fog_type, color_r, color_g, color_b, altitude_min, altitude_max, range_min, range_max, ) = reader.read(cls._STRUCT) assert_eq("name", DEFAULT_FOG_NAME, name_raw, reader.prev + 0) # see LIGHT_STATE - in this case, all FOG_STATEs use the same flags # 1 << 0 set fog state # 1 << 1 set fog color # 1 << 2 set fog altitude # 1 << 3 set fog range assert_eq("flag", 14, flag_raw, reader.prev + 32) # OFF = 0, LINEAR = 1, EXPONENTIAL = 2 assert_eq("fog type", 1, fog_type, reader.prev + 36) assert_between("red", 0.0, 1.0, color_r, reader.prev + 40) assert_between("green", 0.0, 1.0, color_g, reader.prev + 44) assert_between("blue", 0.0, 1.0, color_b, reader.prev + 48) # altitude is always ordered this way even negative (unlike range) assert_ge("altitude max", altitude_min, altitude_max, reader.prev + 56) assert_ge("range min", 0.0, range_min, reader.prev + 60) assert_ge("range max", range_min, range_max, reader.prev + 64) return cls( fog_type="LINEAR", color=(color_r, color_g, color_b), altitude=(altitude_min, altitude_max), range=(range_min, range_max), )
def _read_node_data_light( # pylint: disable=too-many-locals reader: BinReader, ) -> Light: ( direction_x, # 000 direction_y, # 004 direction_z, # 008 trans_x, # 012 trans_y, # 016 trans_z, # 020 zero024, # 112 zero bytes one136, zero140, zero144, zero148, zero152, diffuse, # 156 ambient, # 160 color_r, # 164 color_g, # 168 color_b, # 172 flag_raw, # 176 range_min, # 180 range_max, # 184 range_min_sq, # 188 range_max_sq, # 192 range_inv, # 196 parent_count, # 200 parent_ptr, # 204 zero208, ) = reader.read(LIGHT) # translation is never set assert_eq("trans x", 0.0, trans_x, reader.prev + 12) assert_eq("trans y", 0.0, trans_y, reader.prev + 16) assert_eq("trans z", 0.0, trans_z, reader.prev + 20) assert_all_zero("field 024", zero024, reader.prev + 24) assert_eq("field 136", 1.0, one136, reader.prev + 136) assert_eq("field 140", 0.0, zero140, reader.prev + 140) assert_eq("field 144", 0.0, zero144, reader.prev + 144) assert_eq("field 148", 0.0, zero148, reader.prev + 148) assert_eq("field 152", 0.0, zero152, reader.prev + 152) assert_between("diffuse", 0.0, 1.0, diffuse, reader.prev + 156) assert_between("ambient", 0.0, 1.0, ambient, reader.prev + 160) assert_eq("color r", 1.0, color_r, reader.prev + 164) assert_eq("color g", 1.0, color_g, reader.prev + 168) assert_eq("color b", 1.0, color_b, reader.prev + 172) with assert_flag("flag", flag_raw, reader.prev + 176): flag = LightFlag.check(flag_raw) assert_eq("flag", LIGHT_FLAG, flag, reader.prev + 176) assert_gt("range min", 0.0, range_min, reader.prev + 180) assert_gt("range max", range_min, range_max, reader.prev + 184) expected = range_min * range_min assert_eq("range min sq", expected, range_min_sq, reader.prev + 188) expected = range_max * range_max assert_eq("range max sq", expected, range_max_sq, reader.prev + 192) expected = force_single_prec(1.0 / (range_max - range_min)) assert_eq("range inv", expected, range_inv, reader.prev + 196) # if this was ever zero, field 208 wouldn't be read assert_eq("parent count", 1, parent_count, reader.prev + 200) assert_ne("parent ptr", 0, parent_ptr, reader.prev + 204) assert_eq("field 208", 0, zero208, reader.prev + 208) # light nodes always have an action priority of 9 return Light( type="Light", direction=(direction_x, direction_y, direction_z), diffuse=diffuse, ambient=ambient, color=(color_r, color_g, color_b), range=(range_min, range_max), parent_ptr=parent_ptr, )
def _read_node_data_camera( # pylint: disable=too-many-locals reader: BinReader, ) -> Camera: ( world_index, # 000 window_index, # 004 focus_node_xy, # 008 focus_node_xz, # 012 flag_raw, # 016 trans_x, # 020 trans_y, # 024 trans_z, # 028 rot_x, # 032 rot_y, # 036 rot_z, # 040 zero044, # 132 zero bytes clip_near_z, # 176 clip_far_z, # 180 zero184, # 24 zero bytes lod_multiplier, # 208 lod_inv_sq, # 212 fov_h_zoom_factor, # 216 fov_v_zoom_factor, # 220 fov_h_base, # 224 fov_v_base, # 228 fov_h, # 232 fov_v, # 236 fov_h_half, # 240 fov_v_half, # 244 one248, zero252, # 60 zero bytes one312, zero316, # 72 zero bytes one388, zero392, # 72 zero bytes zero464, fov_h_tan_inv, # 468 fov_v_tan_inv, # 472 stride, zone_set, unk484, ) = reader.read(CAMERA) assert_eq("world index", 0, world_index, reader.prev + 0) assert_eq("window index", 1, window_index, reader.prev + 4) assert_eq("focus node xy", -1, focus_node_xy, reader.prev + 8) assert_eq("focus node xz", -1, focus_node_xz, reader.prev + 12) assert_eq("flag", 0, flag_raw, reader.prev + 16) assert_eq("trans x", 0.0, trans_x, reader.prev + 20) assert_eq("trans y", 0.0, trans_y, reader.prev + 24) assert_eq("trans z", 0.0, trans_z, reader.prev + 28) assert_eq("rot x", 0.0, rot_x, reader.prev + 32) assert_eq("rot y", 0.0, rot_y, reader.prev + 36) assert_eq("rot z", 0.0, rot_z, reader.prev + 40) # WorldTranslate: Vec3 # WorldRotate: Vec3 # MtwMatrix: Mat # Unk: Vec3 # ViewVector: Vec3 # Matrix: Mat # AltTranslate: Vec3 assert_all_zero("field 044", zero044, reader.prev + 44) assert_gt("clip near z", 0.0, clip_near_z, reader.prev + 176) assert_gt("clip far z", clip_near_z, clip_far_z, reader.prev + 180) assert_all_zero("field 184", zero184, reader.prev + 184) assert_eq("LOD mul", 1.0, lod_multiplier, reader.prev + 208) assert_eq("LOD inv sq", 1.0, lod_inv_sq, reader.prev + 212) assert_eq("FOV H zoom factor", 1.0, fov_h_zoom_factor, reader.prev + 216) assert_eq("FOV V zoom factor", 1.0, fov_v_zoom_factor, reader.prev + 220) assert_gt("FOV H base", 0.0, fov_h_base, reader.prev + 224) assert_gt("FOV V base", 0.0, fov_v_base, reader.prev + 228) assert_eq("FOV H zoomed", fov_h_base, fov_h, reader.prev + 232) assert_eq("FOV V zoomed", fov_v_base, fov_v, reader.prev + 236) assert_eq("FOV H half", fov_h / 2.0, fov_h_half, reader.prev + 240) assert_eq("FOV V half", fov_v / 2.0, fov_v_half, reader.prev + 244) assert_eq("field 248", 1, one248, reader.prev + 248) assert_all_zero("field 252", zero252, reader.prev + 252) assert_eq("field 312", 1, one312, reader.prev + 312) assert_all_zero("field 316", zero316, reader.prev + 316) assert_eq("field 388", 1, one388, reader.prev + 388) assert_all_zero("field 392", zero392, reader.prev + 392) assert_eq("field 464", 0, zero464, reader.prev + 464) expected = force_single_prec(1.0 / tan(fov_h_half)) assert_eq("FOV H tan inv", expected, fov_h_tan_inv, reader.prev + 468) expected = force_single_prec(1.0 / tan(fov_v_half)) assert_eq("FOV V tan inv", expected, fov_v_tan_inv, reader.prev + 472) assert_eq("stride", 0, stride, reader.prev + 476) assert_eq("zone set", 0, zone_set, reader.prev + 480) assert_eq("field 484", -256, unk484, reader.prev + 484) return Camera(type="Camera", clip=(clip_near_z, clip_far_z), fov=(fov_h_base, fov_v_base))
def _assert_matrix(expected: Matrix) -> None: assert_eq("matrix 00", expected[0], matrix00, reader.prev + 48) assert_eq("matrix 01", expected[1], matrix01, reader.prev + 52) assert_eq("matrix 02", expected[2], matrix02, reader.prev + 56) assert_eq("matrix 10", expected[3], matrix10, reader.prev + 60) assert_eq("matrix 11", expected[4], matrix11, reader.prev + 64) assert_eq("matrix 12", expected[5], matrix12, reader.prev + 68) assert_eq("matrix 20", expected[6], matrix20, reader.prev + 72) assert_eq("matrix 21", expected[7], matrix21, reader.prev + 76) assert_eq("matrix 22", expected[8], matrix22, reader.prev + 80)