-
-
Save fireundubh/f8168945dddd613a9f2b79942628cf82 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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