Source code for bethesda_structs.plugin.fnv

# Copyright (c) 2018 Stephen Bunn <stephen@bunn.io>
# MIT License <https://choosealicense.com/licenses/mit/>

import io
import os
from typing import List, Generator

from construct import (
    If,
    Enum,
    Bytes,
    Const,
    Int8sl,
    Struct,
    Switch,
    Int16sl,
    Int16ul,
    Int32sl,
    Int32ul,
    Padding,
    Computed,
    Construct,
    Container,
    FlagsEnum,
    LazyBound,
    Compressed,
    IfThenElse,
    GreedyBytes,
    GreedyRange,
    PaddedString,
)
from multidict import CIMultiDict

from ._common import FNVFormID
from .records import RecordMapping
from .._common import BasePlugin


[docs]class FNVPlugin(BasePlugin): """The plugin for Fallout: New Vegas. This plugin structure *should* handle plugins for the games: - Fallout 3 - Fallout: New Vegas Note: This structure *currently* reads all data on initialization. This may appear as *slower* initialization times for larger plugins. Should be fixed by `lazy constructs <https://construct.readthedocs.io/en/latest/lazy.html>`_ when they become more stable. **Credit:** - `FopDoc <https://tes5edit.github.io/fopdoc/FalloutNV/Records.html>`_ """ subrecord_struct = Struct( "type" / PaddedString(4, "utf8"), "data_size" / Int16ul, "data" / Bytes(lambda this: this.data_size), "parsed" / Computed( lambda this: FNVPlugin.parse_subrecord( this._.id, this._.type, this.type, this.data ) ), ) """The structure for FO3/FNV subrecords. Returns: :class:`~construct.core.Struct`: The structure of FO3/FNV subrecords """ record_struct = Struct( "type" / PaddedString(4, "utf8"), "data_size" / Int32ul, "flags" / FlagsEnum( Int32ul, master=0x00000001, _unknown_0=0x00000002, _unknown_1=0x00000004, _unknown_2=0x00000008, form_initialized=0x00000010, deleted=0x00000020, constant=0x00000040, fire_disabled=0x00000080, inaccessible=0x00000100, casts_shadows=0x00000200, persistent=0x00000400, initially_disabled=0x00000800, ignored=0x00001000, no_voice_filter=0x00002000, cannot_save=0x00004000, visible_when_distant=0x00008000, random_anim_start=0x00010000, dangerous=0x00020000, compressed=0x00040000, cant_wait=0x00080000, _unknown_3=0x00100000, _unknown_4=0x00200000, _unknown_5=0x00400000, _unknown_6=0x00800000, destructible=0x01000000, obstacle=0x02000000, navmesh_filter=0x04000000, navmesh_box=0x08000000, non_pipboy=0x10000000, child_can_use=0x20000000, navmesh_ground=0x40000000, _unknown_7=0x80000000, ), "id" / Int32ul, "revision" / Int32ul, "version" / Int16ul, "_unknown_0" / Int16ul, # NOTE: ignores compressed data size as it is handled by GreedyBytes If(lambda this: this.flags.compressed, Padding(4)), "data" / IfThenElse( lambda this: this.flags.compressed, Compressed(Bytes(lambda this: this.data_size), "zlib"), Bytes(lambda this: this.data_size), ), "subrecords" / Computed( lambda this: GreedyRange(FNVPlugin.subrecord_struct).parse( this.data, id=this.id, type=this.type ) ), ) """The structure for FO3/FNV records. Returns: :class:`~construct.core.Struct`: The structure of FO3/FNV records """ # TODO: instead of using ``GreedyRange`` to handle parsing unknown length lists, # should probably use other repeaters to avoid messy construct debugging # (will always raise exception when expects record type to exist, but gets 0 bytes) group_struct = Struct( "type" / Const(b"GRUP"), "group_size" / Int32ul, # TODO: find a better way of lazily building ``label`` in place # instead of computing it later # NOTE: deferred until group_type is determined "_label" / Bytes(4), "group_type" / Enum( Int32sl, top_level=0, world_children=1, interior_cell_block=2, interior_cell_subblock=3, exterior_cell_block=4, exterior_cell_subblock=5, cell_children=6, topic_children=7, cell_persistent_children=8, cell_temporary_children=9, cell_visible_distant_children=10, ), "label" / Computed( lambda this: Switch( this.group_type, { "top_level": PaddedString(4, "utf8"), "world_children": FNVFormID(["WRLD"]), "interior_cell_block": Int32sl, "interior_cell_subblock": Int32sl, "exterior_cell_block": Struct("y" / Int8sl, "x" / Int8sl), "exterior_cell_subblock": Struct("y" / Int8sl, "x" / Int8sl), "cell_children": FNVFormID(["CELL"]), "topic_children": FNVFormID(["DIAL"]), "cell_persistent_children": FNVFormID(["CELL"]), "cell_temporary_children": FNVFormID(["CELL"]), "cell_visible_distant_children": FNVFormID(["CELL"]), }, default=GreedyBytes, ).parse(this._label) ), "stamp" / Int16ul, "_unknown_0" / Bytes(6), "data" / Bytes(lambda this: this.group_size - 24), "subgroups" / If( lambda this: (len(this.data) > 4 and this.data[:4] == b"GRUP"), Computed( lambda this: GreedyRange( LazyBound(lambda: FNVPlugin.group_struct) ).parse(this.data) ), ), "records" / If( lambda this: this.subgroups is None, Computed( lambda this: GreedyRange(FNVPlugin.record_struct).parse(this.data) ), ), ) """The structure for FO3/FNV groups. Returns: :class:`~construct.core.Struct`: The structure of FO3/FNV groups """ plugin_struct = Struct( "header" / record_struct * "Plugin header record", "groups" / GreedyRange(group_struct) * "Plugin groups", ) """The structure for FO3/FNV plugins. Returns: :class:`~construct.core.Struct`: The structure of FO3/FNV plugins """ # NOTE: working record is mangaled in order to protect state during # subrecord parsing for record state __working_record = {}
[docs] @classmethod def can_handle(cls, filepath: str) -> bool: """Determines if a given file can be handled by the plugin. Args: filepath (str): The filepath to evaluate Raises: FileNotFoundError: When the given `filepath` cannot be found Returns: bool: True if file can be handled, otherwise False """ if not os.path.isfile(filepath): raise FileNotFoundError(f"file {filepath!r} does not exist") header = cls.record_struct.parse_file(filepath) # NOTE: must clear class working record after every "full" file parse # otherwise, subsequent parses will have fragmented data when trying to discover # and parse subrecords cls.__working_record = {} return header.type == "TES4" and header.version == 15
[docs] @classmethod def parse_subrecord( cls, record_id: int, record_type: str, subrecord_type: str, subrecord_data: bytes, strict: bool = True, ) -> Container: """Parses a subrecord's data. Args: record_type (str): The parent record type subrecord_type (str): The subrecord type subrecord_data (bytes): The subrecord data to parse strict (bool): Defaults to True, If True, enforce strict subrecord discovery Returns: Container: The resulting parsed container """ (record_type, subrecord_type) = (record_type.upper(), subrecord_type.upper()) # handle reset of working record state if record_id not in cls.__working_record: cls.__working_record[record_id] = [] record_subrecords = RecordMapping.get(record_type) if record_subrecords: (parsed, working_record) = record_subrecords.handle_working( subrecord_type, subrecord_data, cls.__working_record[record_id], strict=strict, ) cls.__working_record[record_id].extend(working_record) return parsed