Skip to content

Instantly share code, notes, and snippets.

@fireundubh
Last active December 31, 2017 13:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fireundubh/f8168945dddd613a9f2b79942628cf82 to your computer and use it in GitHub Desktop.
Save fireundubh/f8168945dddd613a9f2b79942628cf82 to your computer and use it in GitHub Desktop.
# coding=utf-8
import io
import os
import regex
import sys
DEBUG = False
def read_int32(data):
return int.from_bytes(data.read(4), byteorder=sys.byteorder, signed=True)
def read_short(data):
return int.from_bytes(data.read(2), byteorder=sys.byteorder, signed=True)
def read_signature(data):
return data.read(4).decode('utf-8')
def main():
file = r'E:\SteamLibrary\Skyrim Special Edition\Data\HearthFires.esm'
# file = r'E:\SteamLibrary\Skyrim Special Edition\Data\test.esp'
re = regex.compile('^[A-Z_]{4}')
blocks = []
with open(file, mode='rb') as f:
file_extension = os.path.splitext(os.path.basename(file))[1]
if file_extension not in ['.esp', '.esm']:
raise ValueError('File extension did not match expected value: .esp, or .esm', file_extension)
signature = read_signature(f)
if signature != 'TES4':
raise ValueError('File magic did not match expected value: TES4', signature)
else:
# skip file header
if file_extension == '.esp':
f.seek(read_int32(f) + 2, 1)
elif file_extension == '.esm':
f.seek(read_int32(f) + 16, 1)
# ESPs and ESMs have slightly different headers
if file_extension == '.esp':
signature = read_signature(f)
if signature != 'DATA':
raise ValueError('Signature did not match expected value: DATA', signature)
else:
# skip DATA to first GRUP
data_size = read_short(f)
f.seek(data_size, 1)
signature = read_signature(f)
if signature != 'GRUP':
raise ValueError('Signature did not match expected value: GRUP', signature)
else:
f.seek(-4, 1)
while True:
if f.tell() >= os.path.getsize(file):
break
signature = read_signature(f)
data_size = read_int32(f)
f.seek(-8, 1)
data_block = f.read(data_size)
group = io.BytesIO(data_block)
blocks.append(group)
# print(f'{f.tell()}/{os.path.getsize(file)} = {signature}:{data_size}')
# iterate through blocks
for block in blocks:
# GRUP
block_signature = read_signature(block)
# [GRUP] Data Size
block_size = read_int32(block)
# Signature of records in GRUP
group_signature = read_signature(block)
if group_signature in ['CELL', 'WRLD', 'DIAL']:
# CELL, WRLD, and DIAL groups have groups within groups within groups - skipping for now
# TODO: Validate block data to ensure no data loss from blocks[] generation
# TODO: Iterate through groups, perhaps recursively?
continue
if re.match(group_signature) is None:
raise ValueError('Group signature did not match expected pattern: ^[A-Z]{4}', group_signature)
if DEBUG:
print('[Block] {} ({}): {}'.format(block_signature, block_size, group_signature))
# unknown and irrelevant data
block.seek(12, 1)
# iterate through records in group and output some record header data
while True:
if block.tell() >= block_size:
break
# [Field] Signature
record_signature = read_signature(block)
# [Field] Data Size
data_size = read_int32(block)
# [Field] Record Flags
block.seek(4, 1)
# [Field] Form ID
form_id = '{:08x}'.format(read_int32(block))
# [Field] Version Control Info 1
block.seek(4, 1)
# [Field] Form Version
form_version = read_short(block)
# [Field] Version Control Info 2
block.seek(2, 1)
# skip to next record
block.seek(data_size, 1)
print(f'[Record] [{record_signature}:{form_id.upper()}] Form Version: {form_version}')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment