Source code for esp_parser.subrecords

#!/usr/bin/env python3
#
#  subrecords.py
"""
Subrecord types used by multiple records.
"""
#
#  Copyright © 2024 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#

# stdlib
import struct
from io import BytesIO
from typing import List, NamedTuple, Tuple, Type

# 3rd party
import attrs
from typing_extensions import Self

# this package
from esp_parser.types import (
		BytesRecordType,
		Collection,
		CStringRecord,
		FormIDRecord,
		Int32Record,
		IntEnum,
		MarkerRecord,
		RecordType,
		StructRecord
		)
from esp_parser.utils import NULL, namedtuple_qualname_repr

__all__ = (
		"ACBS",
		"AIDT",
		"AidtAggroEnum",
		"AidtAssistanceEnum",
		"AidtConfidenceEnum",
		"AidtMoodEnum",
		"BMDT",
		"CTDA",
		"Destruction",
		"DialType",
		"DNAM",
		"EDID",
		"Effect",
		"InfoNextSpeaker",
		"Item",
		"Model",
		"OBND",
		"PositionRotation",
		"Script",
		"SkillEnum",
		"XNAM",
		"XnamCombatReactionEnum",
		)


[docs]class EDID(CStringRecord): """ Editor ID. """
[docs]@attrs.define class CTDA(RecordType): """ Condition. """ type: int # see https://tes5edit.github.io/fopdoc/Fallout3/Records/Subrecords/CTDA.html unused: bytes #: A form ID or a float32 value comparison_value: bytes #: Function index. function: int # see https://tes5edit.github.io/fopdoc/Fallout3/Records/Subrecords/CTDA.html #: First parameter to pass to the function. param1: bytes #: Second parameter to pass to the function. param2: bytes # See https://tes5edit.github.io/fopdoc/Fallout3/Records/Subrecords/CTDA.html run_on: int reference: bytes """ A form ID of a :class:`~.ACHR`, :class:`~.ACRE`, :class:`~.REFR`, :class:`~.PMIS` or :class:`~.PGRE` reference on which to apply the function, or null. """ # Also refers to :class:`~.PLYR` which doesn't exist.
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x1c\x00" # size field return cls( type=struct.unpack(">B", raw_bytes.read(1))[0], unused=raw_bytes.read(3), comparison_value=raw_bytes.read(4), function=struct.unpack("<I", raw_bytes.read(4))[0], param1=raw_bytes.read(4), param2=raw_bytes.read(4), run_on=struct.unpack(">I", raw_bytes.read(4))[0], reference=raw_bytes.read(4), )
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ return b"".join([ b"CTDA\x1c\x00", struct.pack(">B", self.type), self.unused, self.comparison_value, struct.pack("<I", self.function), self.param1, self.param2, struct.pack(">I", self.run_on), self.reference, ])
[docs]class Model(Collection): """ Subrecords for models. """ members = { b"MODL", b"MOD2", b"MOD3", b"MOD4", b"MODB", b"MODT", b"MO2T", b"MO3T", b"MO4T", b"MODS", b"MO2S", b"MO3S", b"MO4S", }
[docs] class MODL(CStringRecord): """ Model Filename. """
[docs] class MOD2(MODL): """ Model Filename (2nd instance). """
[docs] class MOD3(MODL): """ Model Filename (3rd instance). """
[docs] class MOD4(MODL): """ Model Filename (4th instance). """
[docs] class MODB(FormIDRecord): # noqa: D106 # TODO pass
[docs] class MODT(List[int], RecordType): """ Texture File Hashes. """ def __repr__(self) -> str: return f"{self.__class__.__qualname__}({super().__repr__()})"
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ size = struct.unpack("<H", raw_bytes.read(2))[0] return cls(struct.unpack(f"<{size}B", raw_bytes.read(size)))
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ size = len(self) size_field = struct.pack("<H", size) body = struct.pack(f"<{size}B", *self) return b"NIFT" + size_field + body
[docs] class MO2T(MODT): """ Texture File Hashes (2nd instance). """
[docs] class MO3T(MODT): """ Texture File Hashes (3rd instance). """
[docs] class MO4T(MODT): """ Texture File Hashes (4th instance). """
[docs] @attrs.define class AlternateTexture: """ A texture in a :class:`~Model.MODS`. """ #: 3D Name name: bytes #: New Texture. Form ID of a :class:`~.TXST` record. texture: bytes index: int
[docs] @classmethod def unpack(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Unpack bytes for the :class:`~.Model.AlternateTexture`. :param raw_bytes: """ alt_texture_name_length = struct.unpack("<I", raw_bytes.read(4))[0] alt_texture_3d_name = raw_bytes.read(alt_texture_name_length) alt_texture_new_texture, alt_texture_3d_index = struct.unpack("<4si", raw_bytes.read(8)) return cls( alt_texture_3d_name, alt_texture_new_texture, alt_texture_3d_index, )
[docs] def pack(self) -> bytes: """ Pack the :class:`~.Model.AlternateTexture` to bytes. """ name_length = len(self.name) return struct.pack(f"<I{name_length}s4si", name_length, self.name, self.texture, self.index)
[docs] class MODS(List[AlternateTexture], RecordType): """ List of alternate textures. """ def __repr__(self) -> str: return f"{self.__class__.__qualname__}({super().__repr__()})"
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ size, count = struct.unpack("<HI", raw_bytes.read(6)) buf = BytesIO(raw_bytes.read(size - 4)) # -4 for the count field already read alt_textures = cls() for _ in range(count): alt_textures.append(Model.AlternateTexture.unpack(buf)) assert not buf.read() assert len(alt_textures) == count return alt_textures
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ alt_textures = b"".join(at.pack() for at in self) alt_textures_size = len(alt_textures) size = alt_textures_size + 4 # +4 for count field count = len(self) return b"MODS" + struct.pack("<HI", size, count) + alt_textures
[docs] class MO2S(MODS): """ List of alternate textures (2nd instance). """
[docs] class MO3S(MODS): """ List of alternate textures (2nd instance). """
[docs] class MO4S(MODS): """ List of alternate textures (2nd instance). """
[docs]class Script: """ Subrecords for scripts. """
[docs] @attrs.define class SCHR(RecordType): """ Basic Script Data. """ unused: bytes = NULL ref_count: int = 0 compiled_size: int = 0 variable_count: int = 0 type: int = 0 # TODO: enum flags: int = 1
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x14\x00" # size field return cls( raw_bytes.read(4), *struct.unpack("<IIIHH", raw_bytes.read(16)), )
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ packed_body = struct.pack( "<IIIHH", self.ref_count, self.compiled_size, self.variable_count, self.type, self.flags, ) return b"SCHR\x14\x00" + self.unused + packed_body
[docs] class SCDA(BytesRecordType): """ Compiled Script Source. """
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ return cls(raw_bytes.read(*struct.unpack("<H", raw_bytes.read(2))))
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ buffer_size = len(self) size_field = struct.pack("<H", buffer_size) return b"SCDA" + size_field + self
[docs] class SCTX(BytesRecordType): """ Script Source. """
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ return cls(raw_bytes.read(*struct.unpack("<H", raw_bytes.read(2))))
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ buffer_size = len(self) size_field = struct.pack("<H", buffer_size) return b"SCTX" + size_field + self
[docs] @attrs.define class SLSD(RecordType): """ Local Variable Data. """ index: int unused: bytes = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' flags: int = 1 unused_: bytes = b'\x00\x00\x00\x00\x00\x00\x00'
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x18\x00" return cls(*struct.unpack("<I12sB7s", raw_bytes.read(24)))
# return cls( # *struct.unpack("<I", raw_bytes.read(4)), # raw_bytes.read(12), # *struct.unpack("<B", raw_bytes.read(1)), # raw_bytes.read(7), # )
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ index_pack = struct.pack("<I", self.index) flags_pack = struct.pack("<B", self.flags) return b"SLSD" + b"\x18\x00" + index_pack + self.unused + flags_pack + self.unused_
[docs] class SCVR(CStringRecord): """ Local Variable Name. """
[docs] class SCRV(Int32Record): """ Referenced Variable. """
# Maybe?
[docs] class SCRO(FormIDRecord): """ Reference. A local variable reference, or the form ID of an :class:`~.ACTI`, :class:`~.DOOR`, :class:`~.STAT`, :class:`~.FURN`, :class:`~.CREA`, :class:`~.SPEL`, :class:`~.NPC_`, :class:`~.CONT`, :class:`~.ARMO`, :class:`~.AMMO`, :class:`~.MISC`, :class:`~.WEAP`, :class:`~.IMAD`, :class:`~.BOOK`, :class:`~.KEYM`, :class:`~.ALCH`, :class:`~.LIGH`, :class:`~.QUST`, :class:`~.PACK`, :class:`~.LVLI`, :class:`~.ECZN`, :class:`~.EXPL`, :class:`~.FLST`, :class:`~.IDLM`, :class:`~.PMIS`, :class:`~.FACT`, :class:`~.ACHR`, :class:`~.REFR`, :class:`~.ACRE`, :class:`~.GLOB`, :class:`~.DIAL`, :class:`~.CELL`, :class:`~.SOUN`, :class:`~.MGEF`, :class:`~.WTHR`, :class:`~.CLAS`, :class:`~.EFSH`, :class:`~.RACE`, :class:`~.LVLC`, :class:`~.CSTY`, :class:`~.WRLD`, :class:`~.SCPT`, :class:`~.IMGS`, :class:`~.MESG`, :class:`~.MSTT`, :class:`~.MUSC`, :class:`~.NOTE`, :class:`~.PERK`, :class:`~.PGRE`, :class:`~.PROJ`, :class:`~.LVLN`, :class:`~.WATR`, :class:`~.ENCH`, :class:`~.TREE`, :class:`~.TERM`, :class:`~.HAIR`, :class:`~.EYES`, :class:`~.ADDN` record, or null. """
# Also refers to :class:`~.PLYR` which doesn't exist.
[docs]class OBND(NamedTuple): """ Object Bounds. """ X1: int = 0 Y1: int = 0 Z1: int = 0 X2: int = 0 Y2: int = 0 Z2: int = 0
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x0c\x00" # size field X1, Y1, Z1, X2, Y2, Z2 = struct.unpack("<hhhhhh", raw_bytes.read(12)) return cls(X1, Y1, Z1, X2, Y2, Z2)
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ return b"OBND\x0c\x00" + struct.pack("<hhhhhh", *self)
RecordType.register(OBND)
[docs]@attrs.define class ACBS(RecordType): """ Configuration. Used by the :class:`~.CREA` and :class:`~.NPC_` record types. """ flags: int # See https://tes5edit.github.io/fopdoc/Fallout3/Records/Subrecords/ACBS.html fatigue: int barter_gold: int level_or_level_mult: int """ Level or level multiplier. If the 0x00000080 flag is set, the value is divided by 1000 to give a multiplier. """ calc_min: int calc_max: int speed_multiplier: int #: Karma (Alignment) karma: float disposition_base: int template_flags: int # See https://tes5edit.github.io/fopdoc/Fallout3/Records/Subrecords/ACBS.html
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x18\x00" # size field return cls(*struct.unpack("<IHHhHHHfhH", raw_bytes.read(24)))
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ packed = struct.pack( "<IHHhHHHfhH", self.flags, self.fatigue, self.barter_gold, self.level_or_level_mult, self.calc_min, self.calc_max, self.speed_multiplier, self.karma, self.disposition_base, self.template_flags, ) return b"ACBS\x18\x00" + packed
[docs]class AidtAggroEnum(IntEnum): """ Enum for ``AIDT.aggression``. """ Unaggressive = 0 Aggressive = 1 VeryAggressive = 2 Frenzied = 3
[docs]class AidtConfidenceEnum(IntEnum): """ Enum for ``AIDT.confidence``. """ Cowardly = 0 Cautious = 1 Average = 2 Brave = 3 Foolhardy = 4
[docs]class AidtMoodEnum(IntEnum): """ Enum for ``AIDT.mood``. """ Neutral = 0 Afraid = 1 Annoyed = 2 Cocky = 3 Drugged = 4 Pleasant = 5 Angry = 6 Sad = 7
[docs]class AidtAssistanceEnum(IntEnum): """ Enum for ``AIDT.assistance``. """ Nobody = 0 Allies = 1 FriendsAllies = 2
[docs]class SkillEnum(IntEnum): """ Enum for Skills (e.g. AI teaching, skill book). As used in the :class:`~.AIDT`, :class:`~.BOOK` and :class:`~.CLAS` record types. """ NONE = -1 Barter = 0 BigGuns = 1 EnergyWeapons = 2 Explosives = 3 Lockpick = 4 Medicine = 5 MeleeWeapons = 6 Repair = 7 Science = 8 SmallGuns = 9 Sneak = 10 Speech = 11 Throwing = 12 # unused Unarmed = 13
[docs]@attrs.define class AIDT(RecordType): """ AI Data. Used by the :class:`~.CREA` and :class:`~.NPC_` record types. """ aggression: AidtAggroEnum confidence: AidtConfidenceEnum energy_level: int responsibility: int mood: AidtMoodEnum unused: bytes buys_sells_services_flags: int # see https://tes5edit.github.io/fopdoc/Fallout3/Records/Values/Services.html teaches: SkillEnum max_training_level: int assistance: AidtAssistanceEnum aggro_radius_behaviour_flags: int # See https://tes5edit.github.io/fopdoc/Fallout3/Records/Subrecords/AIDT.html aggro_radius: int
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x14\x00" # size field unpacked = struct.unpack("<BBBBB3sIbBbBi", raw_bytes.read(20)) aggression = AidtAggroEnum(unpacked[0]) confidence = AidtConfidenceEnum(unpacked[1]) mood = AidtMoodEnum(unpacked[4]) teaches = SkillEnum(unpacked[7]) assistance = AidtAssistanceEnum(unpacked[9]) return cls( aggression=aggression, confidence=confidence, energy_level=unpacked[2], responsibility=unpacked[3], mood=mood, unused=unpacked[5], buys_sells_services_flags=unpacked[6], teaches=teaches, max_training_level=unpacked[8], assistance=assistance, aggro_radius_behaviour_flags=unpacked[10], aggro_radius=unpacked[11], )
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ packed = struct.pack( "<BBBBB3sIbBbBi", self.aggression, self.confidence, self.energy_level, self.responsibility, self.mood, self.unused, self.buys_sells_services_flags, self.teaches, self.max_training_level, self.assistance, self.aggro_radius_behaviour_flags, self.aggro_radius, ) return b"AIDT\x14\x00" + packed
[docs]class Item(Collection): """ Subrecords for items. """ members = {b"CNTO", b"COED"}
[docs] class CNTO(NamedTuple): """ Item. """ item: bytes """ Form ID of the item. Form ID of an :class:`~ARMO`, :class:`~AMMO`, :class:`~MISC`, :class:`~WEAP`, :class:`~BOOK`, :class:`~LVLI`, :class:`~KEYM`, :class:`~ALCH`, :class:`~NOTE`, :class:`~MSTT` or :class:`~STAT` record. """ item_count: int
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x08\x00" # size field return cls(*struct.unpack("<4si", raw_bytes.read(8)))
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ return b"CNTO\x08\x00" + struct.pack("<4si", *self)
def __repr__(self) -> str: return namedtuple_qualname_repr(self)
[docs] class COED(NamedTuple): """ Extra item data. """ owner: bytes """ Form ID of the owner. Form ID of an :class:`~.NPC_` or :class:`~.FACT` record, or null. """ glob_var_req_rank: bytes """ Form ID of a :class:`~.GLOB` record, an integer representing the required rank, or null. If an integer representing the required rank it will be stored as the 4 bytes of a uint32 (little endian). """ condition: float
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x0c\x00" # size field return cls(*struct.unpack("<4s4sf", raw_bytes.read(12)))
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ return b"COED\x0c\x00" + struct.pack("<4s4sf", *self)
def __repr__(self) -> str: return namedtuple_qualname_repr(self)
RecordType.register(CNTO) RecordType.register(COED)
[docs]class PositionRotation: """ Subrecord for position/rotation. Used in :class:`~.REFR`, :class:`~.ACHR` and :class:`~.ACRE`. """
[docs] class DATA(NamedTuple): """ Position / Rotation. """ xp: float = 0.0 yp: float = 0.0 zp: float = 0.0 xr: float = 0.0 yr: float = 0.0 zr: float = 0.0
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x18\x00" # size field xp, yp, zp, xr, yr, zr = struct.unpack("<ffffff", raw_bytes.read(24)) return cls(xp, yp, zp, xr, yr, zr)
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ return b"DATA\x18\x00" + struct.pack("<ffffff", *self)
def __repr__(self) -> str: return namedtuple_qualname_repr(self)
RecordType.register(DATA)
[docs]class XnamCombatReactionEnum(IntEnum): """ Group Combat Reaction. """ Neutral = 0 Enemy = 1 Ally = 2 Friend = 3
[docs]@attrs.define class XNAM(RecordType): """ Relation used for :class:`~.FACT` and :class:`~.RACE` records. """ #: Form ID of a :class:`~.FACT` or :class:`~.RACE` record. faction: bytes modifier: int group_combat_reaction: XnamCombatReactionEnum
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x0c\x00" # size field return cls(*struct.unpack("<4siI", raw_bytes.read(12)))
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ return b"XNAM\x0c\x00" + struct.pack("<4siI", self.faction, self.modifier, self.group_combat_reaction)
[docs]class DialType(IntEnum): """ Enum for ``DIAL.DATA.type`` and ``INFO.DATA.type``. """ Topic = 0 Conversation = 1 Combat = 2 Persuasion = 3 Detection = 4 Service = 5 Miscellaneous = 6 Radio = 7
[docs]class InfoNextSpeaker(IntEnum): """ Enum for ``INFO.DATA.next_speaker``. """ Target = 0 Self = 1 Either = 2
[docs]class Destruction(Collection): """ Destruction subrecord collection. """ members = { b"DEST", b"DSTD", b"DSTF", }
[docs] @attrs.define class DEST(RecordType): """ Destruction data header. """ health: int count: int flags: int # See https://tes5edit.github.io/fopdoc/Fallout3/Records/Subrecords/Destruction.html unknown: bytes
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x08\x00" # size field return cls(*struct.unpack("<iBB2s", raw_bytes.read(8)))
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ return b"DEST" + struct.pack( "<HiBB2s", 8, self.health, self.count, self.flags, self.unknown, )
[docs] @attrs.define class DSTD(RecordType): """ Destruction Stage Data. """ health_percentage: int index: int damage_stage: int flags: int # See https://tes5edit.github.io/fopdoc/FalloutNV/Records/Subrecords/Destruction.html self_dps: int #: Form ID of an :class:`~.EXPL` record or null. explosion: bytes #: Form ID of an :class:`~.DEBR` record or null. debris: bytes debris_count: int
[docs] @classmethod def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self: """ Parse this subrecord. :param raw_bytes: Raw bytes for this record """ assert raw_bytes.read(2) == b"\x14\x00" # size field return cls(*struct.unpack("<BBBBi4s4si", raw_bytes.read(20)))
[docs] def unparse(self) -> bytes: """ Turn this subrecord back into raw bytes for an ESP file. """ return b"DSTD" + struct.pack( "<HBBBBi4s4si", 20, self.health_percentage, self.index, self.damage_stage, self.flags, self.self_dps, self.explosion, self.debris, self.debris_count, )
[docs] class DSTF(MarkerRecord): """ Stage End Marker. """
[docs]class Effect(Collection): """ Effect Subrecord Collection. """ members = {b"EFIT", b"EFID"}
[docs] class EFID(FormIDRecord): """ Base effect. Form ID of a :class:`~.MGEF` record. """
[docs] class EfitTypeEnum(IntEnum): """ Enum for ``SPEL.EFIT``. """ Self = 0 Touch = 1 Target = 2
[docs] @attrs.define class EFIT(StructRecord): """ Effect Data. """ magnitude: int area: int duration: int type: "Effect.EfitTypeEnum" = attrs.field(converter=lambda x: Effect.EfitTypeEnum(x)) actor_value: int # See https://tes5edit.github.io/fopdoc/Fallout3/Records/Subrecords/Effect.html
[docs] @staticmethod def get_struct_and_size() -> Tuple[str, int]: """ Returns the pack/unpack struct string and the corresponding size. """ return "<IIIIi", 20
[docs] @staticmethod def get_field_names() -> Tuple[str, ...]: """ Returns a list of attributes on this class in the order they should be packed. """ return ( "magnitude", "area", "duration", "type", "actor_value", )
[docs]@attrs.define class BMDT(StructRecord): """ Biped Data. Used in the :class:`~.ARMO` and :class:`~.ARMA` record types. """ biped_flags: int general_flags: int unused: bytes
[docs] @staticmethod def get_struct_and_size() -> Tuple[str, int]: """ Returns the pack/unpack struct string and the corresponding size. """ return "<IB3s", 8
[docs] @staticmethod def get_field_names() -> Tuple[str, ...]: """ Returns a list of attributes on this class in the order they should be packed. """ return ("biped_flags", "general_flags", "unused")
[docs]@attrs.define class DNAM(StructRecord): """ DNAM record type for :class:`~.ARMA` and :class:`ARMO` in Fallout 3 only. In New Vegas :class:`~.ARMA` and :class:`ARMO` have their own versions. """ #: Value is divided by 100. ar: int flags: int # See https://tes5edit.github.io/fopdoc/FalloutNV/Records/ARMO.html
[docs] @staticmethod def get_struct_and_size() -> Tuple[str, int]: """ Returns the pack/unpack struct string and the corresponding size. """ return "<hH", 4
[docs] @staticmethod def get_field_names() -> Tuple[str, ...]: """ Returns a list of attributes on this class in the order they should be packed. """ return ("ar", "flags")