Package: bethesda-structs
AboutModding Bethesda’s games can be a fine-art.This package intends to providecleanandaccessiblemethods for parsing and understanding Bethesda’s filetypes.For example:There are so many “unarchiver” tools for Bethesda’s archives (.bsaand.ba2), but nogoodprogrammatic way to read these filetypes.
Using this package, understandingevery little detailabout an archive is simple and straight-forward (seeBSA UsageandBA2 Usage).For more advanced usage and information,check out the documentation.The supported filetypes areparsersnotwriters.We do not currently support the writing of archives or plugins.InstallationBecause this is glorious Python, installingbethesda-structsshould be super-duper simple.Using PyPiThe fastest and quickest way to install this packages is by simply usingpipenv(or if you’re oldschoolpip).$pipenvinstallbethesda-structsUsing GitYou can install this package using Git by simply cloning the repo and building the package yourself!$gitclonehttps://github.com/stephen-bunn/bethesda-structs.git$pipenvinstall--dev$pipenvrunpythonsetup.pyinstallUsageUsingbethesda-structsis designed to be straight-forward and intuitive.Below are some short examples of parsing various filetypes.ESPThe ability to parse plugin files is super helpful for understanding the additions and changes that are made to the game.Currently theonlyother real tool that can expose this information to you isTESEditand its sibling applications.This package aims to provide simple, programmatic access to the in-depth details of a plugin!Because of how long it takes to build complete subrecord parers for a given plugin version, theonlycurrently supported plugins are:FNVPlugin- Fallout: New Vegas (partial)F03Plugin- Fallout 3 (partial and experimental)>>> from bethesda_structs.plugin.fnv import FNVPlugin
>>> plugin = FNVPlugin.parse_file('/media/sf_VMShared/esp/fnv/NVWillow.esp')
>>> print(plugin)
FNVPlugin(filepath='/media/sf_VMShared/esp/fnv/NVWillow.esp')
>>>
>>> # print plugin header (is a record)
...
>>> print(plugin.container.header)
Container:
    type = u'TES4' (total 4)
    data_size = 163
    flags = Container:
        master = True
    id = 0
    revision = 0
    version = 15
    data = b'HEDR\x0c\x00\x1f\x85\xab?\x97\x12\x00\x00#\xad'... (truncated, total 163)
    subrecords = ListContainer:
        Container:
            type = u'HEDR' (total 4)
            data_size = 12
            data = b'\x1f\x85\xab?\x97\x12\x00\x00#\xad\r\x00' (total 12)
            parsed = Container:
                value = Container:
                    version = 1.340000033378601
                    num_records = 4759
                    next_object_id = 896291
                description = u'Header' (total 6)
        Container:
            type = u'CNAM' (total 4)
            data_size = 9
            data = b'llamaRCA\x00' (total 9)
            parsed = Container:
                value = u'llamaRCA' (total 8)
                description = u'Author' (total 6)
        Container:
            type = u'SNAM' (total 4)
            data_size = 16
            data = b'NVWillow v.1.10\x00' (total 16)
            parsed = Container:
                value = u'NVWillow v.1.10' (total 15)
                description = u'Description' (total 11)
        Container:
            type = u'MAST' (total 4)
            data_size = 14
            data = b'FalloutNV.esm\x00' (total 14)
            parsed = Container:
                value = u'FalloutNV.esm' (total 13)
                description = u'Master Plugin' (total 13)
        Container:
            type = u'DATA' (total 4)
            data_size = 8
            data = b'\x00\x00\x00\x00\x00\x00\x00\x00' (total 8)
            parsed = Container:
                value = 0
                description = u'File Size' (total 9)
        Container:
            type = u'ONAM' (total 4)
            data_size = 68
            data = b'V\xe3\x0c\x00\xc3\xe3\x0c\x00\xc4\xe3\x0c\x00\xc5\xe3\x0c\x00'... (truncated, total 68)
            parsed = Container:
                value = ListContainer:
                    844630
                    844739
                    844740
                    844741
                    1372461
                    1372463
                    1383111
                    1385321
                    1387301
                    1387302
                    1387303
                    1387304
                    1387906
                    1457771
                    1479505
                    1520201
                    1544392
                description = u'Overridden Records' (total 18)
>>>
>>> # iterate over KEYM records (only 1 in this plugin)
...
>>> for record in plugin.iter_records('KEYM'):
...     print(record)
...
Container:
    type = u'KEYM' (total 4)
    data_size = 279
    flags = Container:
    id = 17415634
    revision = 0
    version = 15
    data = b'EDID\x17\x00WillowNova'... (truncated, total 279)
    subrecords = ListContainer:
        Container:
            type = u'EDID' (total 4)
            data_size = 23
            data = b'WillowNovacBunga'... (truncated, total 23)
            parsed = Container:
                value = u'WillowNovacBungalowKey' (total 22)
                description = u'Editor ID' (total 9)
        Container:
            type = u'OBND' (total 4)
            data_size = 12
            data = b'\xff\xff\xfc\xff\x00\x00\x01\x00\x04\x00\x00\x00' (total 12)
            parsed = Container:
                value = Container:
                    X1 = -1
                    Y1 = -4
                    Z1 = 0
                    X2 = 1
                    Y2 = 4
                    Z2 = 0
                description = u'Object Bounds' (total 13)
        Container:
            type = u'FULL' (total 4)
            data_size = 27
            data = b'Dino Dee-lite Bu'... (truncated, total 27)
            parsed = Container:
                value = u'Dino Dee-lite Bungalow Key' (total 26)
                description = u'Name' (total 4)
        Container:
            type = u'MODL' (total 4)
            data_size = 23
            data = b'Clutter\\Key01Dir'... (truncated, total 23)
            parsed = Container:
                value = u'Clutter\\Key01Dirty.NIF' (total 22)
                description = u'Model Filename' (total 14)
        Container:
            type = u'ICON' (total 4)
            data_size = 48
            data = b'Interface\\Icons\\'... (truncated, total 48)
            parsed = Container:
                value = u'Interface\\Icons\\PipboyImages\\Ite'... (truncated, total 47)
                description = u'Large Icon Filename' (total 19)
        Container:
            type = u'MICO' (total 4)
            data_size = 66
            data = b'Interface\\Icons\\'... (truncated, total 66)
            parsed = Container:
                value = u'Interface\\Icons\\PipboyImages_sma'... (truncated, total 65)
                description = u'Small Icon Filename' (total 19)
        Container:
            type = u'SCRI' (total 4)
            data_size = 4
            data = b'T.\n\x01' (total 4)
            parsed = Container:
                value = FormID(form_id=17444436, forms=['SCPT'])
                description = u'Script' (total 6)
        Container:
            type = u'YNAM' (total 4)
            data_size = 4
            data = b'\xbb\x10\x07\x00' (total 4)
            parsed = Container:
                value = FormID(form_id=463035, forms=['SOUN'])
                description = u'Sound - Pick Up' (total 15)
        Container:
            type = u'ZNAM' (total 4)
            data_size = 4
            data = b'\xbc\x10\x07\x00' (total 4)
            parsed = Container:
                value = FormID(form_id=463036, forms=['SOUN'])
                description = u'Sound - Drop' (total 12)
        Container:
            type = u'DATA' (total 4)
            data_size = 8
            data = b'\x00\x00\x00\x00\x00\x00\x00\x00' (total 8)
            parsed = Container:
                value = Container:
                    value = 0
                    weight = 0.0
                description = u'Data' (total 4)BSABethesda’s default archive structure.>>> from bethesda_structs.archive.bsa import BSAArchive
>>> archive = BSAArchive.parse_file('/media/sf_VMShared/bsa/Campfire.bsa')
>>> print(archive)
BSAArchive(filepath=PosixPath('/media/sf_VMShared/bsa/Campfire.bsa'))
>>>
>>> # print archive header
...
>>> print(archive.container.header)
Container:
    magic = b'BSA\x00' (total 4)
    version = 105
    directory_offset = 36
    archive_flags = Container:
        directories_named = True
        files_named = True
    directory_count = 4
    file_count = 493
    directory_names_length = 50
    file_names_length = 14839
    file_flags = Container:
>>>
>>> # print last directory block, containing 1 file record
...
>>> print(archive.container.directory_blocks[-1])
Container:
    name = u'meshes\\mps\x00' (total 11)
    file_records = ListContainer:
        Container:
            hash = 16183754957220078963
            size = 2384
            offset = 25094933
>>>
>>> # print archived filenames (only first 5, 488 more)
...
>>> print(archive.container.file_names)
ListContainer:
    _camp_objectplacementindicatorthread01.psc
    _camp_objectplacementindicatorthread02.psc
    _camp_objectplacementindicatorthread03.psc
    _camp_tentsitlayscript.psc
    campcampfire.psc
    ...
>>>
>>> # extract archive to directory
...
>>> archive.extract('/home/USER/Downloads')BA2Bethesda’s second archive structure (used in Fallout 4).BTDX archives (BA2) are harder to extract than their previous version BA2.The two available archive subtypes are both supported.General (GNRL)Used to store generic files in a compressed/bundled file.>>> from bethesda_structs.archive.btdx import BTDXArchive
>>> archive = BTDXArchive.parse_file('/media/sf_VMShared/ba2/CheatTerminal - Main.ba2')
>>> print(archive)
BTDXArchive(filepath=PosixPath('/media/sf_VMShared/ba2/CheatTerminal - Main.ba2'))
>>>
>>> # print archive header
...
>>> print(archive.container.header)
Container:
    magic = b'BTDX' (total 4)
    version = 1
    type = u'GNRL' (total 4)
    file_count = 982
    names_offset = 3600179
>>>
>>> # print first archive file entry
...
>>> print(archive.container.files[0])
Container:
    hash = 153050373
    ext = u'pex' (total 3)
    directory_hash = 1081231424
    offset = 35376
    packed_size = 0
    unpacked_size = 887
>>>
>>> # extract archive to directory
...
>>> archive.extract('/home/USER/Downloads')Direct Draw (DX10)Used to store (specifically) Microsoft Direct Draw textures.>>> from bethesda_structs.archive.btdx import BTDXArchive
>>> archive = BTDXArchive.parse_file('/media/sf_VMShared/ba2/AK74m - Textures.ba2')
>>> print(archive)
BTDXArchive(filepath=PosixPath('/media/sf_VMShared/ba2/AK74m - Textures.ba2'))
>>>
>>> # print archive header
...
>>> print(archive.container.header)
Container:
    magic = b'BTDX' (total 4)
    version = 1
    type = u'DX10' (total 4)
    file_count = 116
    names_offset = 329069673
>>>
>>> # print first archive file entry
...
>>> print(archive.container.files[0])
Container:
    header = Container:
        hash = 362144756
        ext = u'dds' (total 3)
        directory_hash = 1416395408
        chunks_count = 4
        chunk_header_size = 24
        height = 2048
        width = 2048
        mips_count = 12
        format = 99
    chunks = ListContainer:
        Container:
            offset = 11136
            packed_size = 2714729
            unpacked_size = 4194304
            start_mip = 0
            end_mip = 0
        Container:
            offset = 2725865
            packed_size = 840614
            unpacked_size = 1048576
            start_mip = 1
            end_mip = 1
        Container:
            offset = 3566479
            packed_size = 217598
            unpacked_size = 262144
            start_mip = 2
            end_mip = 2
        Container:
            offset = 3784077
            packed_size = 71579
            unpacked_size = 87408
            start_mip = 3
            end_mip = 11
>>>
>>> # extract archive to directory
...
>>> archive.extract('/home/USER/Downloads')