From 22507ee99ac63c7233941c9a2341b3fb2b9c6d5f Mon Sep 17 00:00:00 2001 From: Infernio Date: Tue, 13 Oct 2020 01:03:48 +0200 Subject: [PATCH 1/9] TES3: Define ACTI, ALCH, APPA, ARMO and BODY TES3: Define ACTI TES3: Define ALCH TES3: Define APPA TES3: Define ARMO TES3: Define BODY --- Mopy/bash/game/falloutnv/records.py | 1 + Mopy/bash/game/morrowind/records.py | 137 ++++++++++++++++++++++++++-- 2 files changed, 130 insertions(+), 8 deletions(-) diff --git a/Mopy/bash/game/falloutnv/records.py b/Mopy/bash/game/falloutnv/records.py index db9036d1df..6fa06e3671 100644 --- a/Mopy/bash/game/falloutnv/records.py +++ b/Mopy/bash/game/falloutnv/records.py @@ -63,6 +63,7 @@ class MreTes4(MreHeaderBase): ) __slots__ = melSet.getSlotsUsed() +#------------------------------------------------------------------------------ class MreAchr(MelRecord): """Placed NPC.""" rec_sig = b'ACHR' diff --git a/Mopy/bash/game/morrowind/records.py b/Mopy/bash/game/morrowind/records.py index 9a2f3ac446..d124e5419e 100644 --- a/Mopy/bash/game/morrowind/records.py +++ b/Mopy/bash/game/morrowind/records.py @@ -23,18 +23,43 @@ """This module contains the Morrowind record classes. Also contains records and subrecords used for the saves - see MorrowindSaveHeader for more information.""" -from ... import bolt -from ...bolt import cstrip, decode +from ... import bolt, brec +from ...bolt import cstrip, decode, Flags from ...brec import MelBase, MelSet, MelString, MelStruct, MelArray, \ - MreHeaderBase, MelUnion, SaveDecider, MelNull, MelSequential + MreHeaderBase, MelUnion, SaveDecider, MelNull, MelSequential, MelRecord, \ + MelGroup, MelGroups, MelUInt8 +if brec.MelModel is None: -# Utilities + class _MelModel(MelGroup): + def __init__(self): + super(_MelModel, self).__init__(u'model', + MelString(b'MODL', u'modPath')) + + brec.MelModel = _MelModel +from ...brec import MelModel + +#------------------------------------------------------------------------------ +# Utilities ------------------------------------------------------------------- +#------------------------------------------------------------------------------ def _decode_raw(target_str): """Adapted from MelUnicode.loadData. ##: maybe move to bolt/brec?""" return u'\n'.join( decode(x, avoidEncodings=(u'utf8', u'utf-8')) for x in cstrip(target_str).split(b'\n')) +#------------------------------------------------------------------------------ +class MelMWId(MelString): + """Wraps MelString to define a common NAME handler.""" + def __init__(self): + super(MelMWId, self).__init__(b'NAME', u'mw_id') + +#------------------------------------------------------------------------------ +class MelMWFull(MelString): + """Defines FNAM, Morrowind's version of FULL.""" + def __init__(self): + super(MelMWFull, self).__init__(b'FNAM', u'full') + +#------------------------------------------------------------------------------ class MelSavesOnly(MelSequential): """Record element that only loads contents if the input file is a save file.""" @@ -44,12 +69,15 @@ def __init__(self, *elements): False: MelNull(b'ANY') }, decider=SaveDecider()) for element in elements)) -class MelMWId(MelString): - """Wraps MelString to define a common NAME handler.""" +#------------------------------------------------------------------------------ +class MelScriptId(MelString): + """Handles the common SCRI subrecord.""" def __init__(self): - super(MelMWId, self).__init__(b'NAME', u'mw_id') + super(MelScriptId, self).__init__(b'SCRI', u'script_id'), -# Shared (plugins + saves) record classes +#------------------------------------------------------------------------------ +# Shared (plugins + saves) record classes ------------------------------------- +#------------------------------------------------------------------------------ class MreTes3(MreHeaderBase): """TES3 Record. File header.""" rec_sig = b'TES3' @@ -96,3 +124,96 @@ def dumpData(self, record, out): ), ) __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +# Plugins-only record classes ------------------------------------------------- +#------------------------------------------------------------------------------ +class MreActi(MelRecord): + """Activator.""" + rec_sig = b'ACTI' + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelScriptId(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreAlch(MelRecord): + """Potion.""" + rec_sig = b'ALCH' + + _potion_flags = Flags(0, Flags.getNames(u'auto_calc')) + + melSet = MelSet( + MelMWId(), + MelModel(), + MelString(b'TEXT', u'book_text'), + MelScriptId(), + MelMWFull(), + MelStruct(b'ALDT', u'f2I', u'potion_weight', u'potion_value', + (_potion_flags, u'potion_flags')), + MelGroups(u'potion_enchantments', + MelStruct(b'ENAM', u'H2b5I', u'effect_index', u'skill_affected', + u'attribute_affected', u'ench_range', u'ench_area', + u'ench_duration', u'ench_magnitude_min', + u'ench_magnitude_max'), + ), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreAppa(MelRecord): + """Alchemical Apparatus.""" + rec_sig = b'APPA' + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelScriptId(), + MelStruct(b'AADT', u'I2fI', u'appa_type', u'appa_quality', + u'appa_weight', u'appa_value'), + MelString(b'ITEX', u'icon_filename'), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreArmo(MelRecord): + """Armor.""" + rec_sig = b'ARMO' + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelScriptId(), + MelStruct(b'AODT', u'If4I', u'armo_type', u'armo_weight', + u'armo_value', u'armo_health', u'enchant_points', u'armor_rating'), + MelString(b'ITEX', u'icon_filename'), + MelGroups(u'armor_data', + MelUInt8(b'INDX', u'biped_object'), + MelString(b'BNAM', u'armor_name_male'), + MelString(b'CNAM', u'armor_name_female'), + ), + MelString(b'ENAM', u'enchant_name'), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreBody(MelRecord): + """Body Parts.""" + rec_sig = b'BODY' + + _part_flags = Flags(0, Flags.getNames(u'part_female', u'part_playable')) + + melSet = MelSet( + MelMWId(), + MelModel(), + MelString(b'FNAM', u'race_name'), + MelStruct(b'BYDT', u'4B', u'part_index', u'part_vampire', + (_part_flags, u'part_flags'), u'part_type'), + ) + __slots__ = melSet.getSlotsUsed() From b58a56956b3948f78b50c9ccf33ed68785510c16 Mon Sep 17 00:00:00 2001 From: Infernio Date: Tue, 13 Oct 2020 01:28:27 +0200 Subject: [PATCH 2/9] TES3: Define BOOK, BSGN, CELL, CLAS and CLOT TES3: Define BOOK TES3: Define BSGN TES3: Define CELL This is a weird one, since it contains the 'proto-FormIDs'. Might be smarter to handle this with a dedicated class in brec instead? It is pretty cool to see how much of this still exists in newer REFR records though (e.g. I was able to use MelRef3D and MelRefScale from brec as is). Mopy/bash/brec/basic_elements.py: Add type annotation, already caught one bug Mopy/bash/game/skyrim/records.py: Fixup for a random pycharm warning (something about unicode, so I just gave it the proper bytes/unicode separation and the warning went away) TES3: Define CLAS TES3: Define CLOT Also refactor the ITEX and ENAM subrecords, turns out they're very common. --- Mopy/bash/brec/basic_elements.py | 2 + Mopy/bash/brec/common_subrecords.py | 15 +- Mopy/bash/game/morrowind/records.py | 209 ++++++++++++++++++++++++++-- Mopy/bash/game/skyrim/records.py | 67 ++++----- 4 files changed, 244 insertions(+), 49 deletions(-) diff --git a/Mopy/bash/brec/basic_elements.py b/Mopy/bash/brec/basic_elements.py index efed494c42..a03cd81ec9 100644 --- a/Mopy/bash/brec/basic_elements.py +++ b/Mopy/bash/brec/basic_elements.py @@ -550,6 +550,8 @@ class MelStruct(MelBase): """Represents a structure record.""" def __init__(self, subType, struct_format, *elements): + """:type subType: bytes + :type struct_format: unicode""" self.subType, self.struct_format = subType, struct_format self.attrs,self.defaults,self.actions,self.formAttrs = MelBase.parseElements(*elements) # Check for duplicate attrs - can't rely on MelSet.getSlotsUsed only, diff --git a/Mopy/bash/brec/common_subrecords.py b/Mopy/bash/brec/common_subrecords.py index f604cf3c95..a6be545177 100644 --- a/Mopy/bash/brec/common_subrecords.py +++ b/Mopy/bash/brec/common_subrecords.py @@ -298,6 +298,19 @@ def __init__(self, sub_type, attr): MelStruct(sub_type, u'2f', u'time', u'value'), ) +#------------------------------------------------------------------------------ +class MelColor(MelStruct): + """Required Color.""" + def __init__(self, color_sig=b'CNAM'): + super(MelColor, self).__init__(color_sig, u'4B', u'red', u'green', + u'blue', u'unused_alpha') + +class MelColorO(MelOptStruct): + """Optional Color.""" + def __init__(self, color_sig=b'CNAM'): + super(MelColorO, self).__init__(color_sig, u'4B', u'red', u'green', + u'blue', u'unused_alpha') + #------------------------------------------------------------------------------ class MelDescription(MelLString): """Handles a description (DESC) subrecord.""" @@ -572,7 +585,7 @@ def __init__(self, entry_type_val, element): fallback=MelNull(b'NULL')) # ignore #------------------------------------------------------------------------------ -class MelRef3D(MelStruct): +class MelRef3D(MelOptStruct): """3D position and rotation for a reference record (REFR, ACHR, etc.).""" def __init__(self): super(MelRef3D, self).__init__( diff --git a/Mopy/bash/game/morrowind/records.py b/Mopy/bash/game/morrowind/records.py index d124e5419e..8364cb7af9 100644 --- a/Mopy/bash/game/morrowind/records.py +++ b/Mopy/bash/game/morrowind/records.py @@ -27,7 +27,9 @@ from ...bolt import cstrip, decode, Flags from ...brec import MelBase, MelSet, MelString, MelStruct, MelArray, \ MreHeaderBase, MelUnion, SaveDecider, MelNull, MelSequential, MelRecord, \ - MelGroup, MelGroups, MelUInt8 + MelGroup, MelGroups, MelUInt8, MelDescription, MelUInt32, MelColorO,\ + MelOptStruct, MelCounter, MelRefScale, MelOptSInt32, MelRef3D, \ + MelOptFloat, MelOptUInt32, MelIcons if brec.MelModel is None: class _MelModel(MelGroup): @@ -47,6 +49,28 @@ def _decode_raw(target_str): decode(x, avoidEncodings=(u'utf8', u'utf-8')) for x in cstrip(target_str).split(b'\n')) +#------------------------------------------------------------------------------ +class MelArmorData(MelGroups): + """Handles the INDX, BNAM and CNAM subrecords shared by ARMO and CLOT.""" + def __init__(self): + super(MelArmorData, self).__init__(u'armor_data', + MelUInt8(b'INDX', u'biped_object'), + MelString(b'BNAM', u'armor_name_male'), + MelString(b'CNAM', u'armor_name_female'), + ) + +#------------------------------------------------------------------------------ +class MelMWEnchantment(MelString): + """Handles ENAM, Morrowind's version of EITM.""" + def __init__(self): + super(MelMWEnchantment, self).__init__(b'ENAM', u'enchantment') + +#------------------------------------------------------------------------------ +class MelMWIcon(MelIcons): + """Handles the common ITEX record, Morrowind's version of ICON.""" + def __init__(self): + super(MelMWIcon, self).__init__(icon_sig=b'ITEX', mico_attr=u'') + #------------------------------------------------------------------------------ class MelMWId(MelString): """Wraps MelString to define a common NAME handler.""" @@ -59,6 +83,39 @@ class MelMWFull(MelString): def __init__(self): super(MelMWFull, self).__init__(b'FNAM', u'full') +#------------------------------------------------------------------------------ +class MelReference(MelSequential): + """Defines a single 'reference', which is Morrowind's version of REFRs in + later games.""" + def __init__(self): + super(MelReference, self).__init__( + MelUInt32(b'FRMR', u'object_index'), + MelMWId(), + MelBase(b'UNAM', u'ref_blocked_marker'), + MelRefScale(), + MelString(b'ANAM', u'ref_owner'), + MelString(b'BNAM', u'global_variable'), + MelString(b'CNAM', u'ref_faction'), + MelOptSInt32(b'INDX', u'ref_faction_rank'), + MelString(b'XSOL', u'ref_soul'), + MelOptFloat(b'XCHG', u'enchantment_charge'), + ##: INTV should have a decider - uint32 or float, depending on + # object type + MelBase(b'INTV', u'remaining_usage'), + MelOptUInt32(b'NAM9', u'gold_value'), + MelGroups(u'cell_travel_destinations', + MelStruct(b'DODT', u'6f', u'dest_pos_x', u'dest_pos_y', + u'dest_pos_z', u'dest_rot_x', u'dest_rot_y', + u'dest_rot_z'), + MelString(b'DNAM', u'dest_cell_name'), + ), + MelOptUInt32(b'FLTV', u'lock_level'), + MelString(b'KNAM', u'key_name'), + MelString(b'TNAM', u'trap_name'), + MelBase(b'ZNAM', u'ref_disabled_marker'), + MelRef3D(), + ) + #------------------------------------------------------------------------------ class MelSavesOnly(MelSequential): """Record element that only loads contents if the input file is a save @@ -176,7 +233,7 @@ class MreAppa(MelRecord): MelScriptId(), MelStruct(b'AADT', u'I2fI', u'appa_type', u'appa_quality', u'appa_weight', u'appa_value'), - MelString(b'ITEX', u'icon_filename'), + MelMWIcon(), ) __slots__ = melSet.getSlotsUsed() @@ -192,13 +249,9 @@ class MreArmo(MelRecord): MelScriptId(), MelStruct(b'AODT', u'If4I', u'armo_type', u'armo_weight', u'armo_value', u'armo_health', u'enchant_points', u'armor_rating'), - MelString(b'ITEX', u'icon_filename'), - MelGroups(u'armor_data', - MelUInt8(b'INDX', u'biped_object'), - MelString(b'BNAM', u'armor_name_male'), - MelString(b'CNAM', u'armor_name_female'), - ), - MelString(b'ENAM', u'enchant_name'), + MelMWIcon(), + MelArmorData(), + MelMWEnchantment(), ) __slots__ = melSet.getSlotsUsed() @@ -217,3 +270,141 @@ class MreBody(MelRecord): (_part_flags, u'part_flags'), u'part_type'), ) __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreBook(MelRecord): + """Book.""" + rec_sig = b'BOOK' + + _scroll_flags = Flags(0, Flags.getNames(u'is_scroll')) + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelStruct(b'BKDT', u'f2IiI', u'book_weight', u'book_value', + (_scroll_flags, u'scroll_flags'), u'skill_id', u'enchant_points'), + MelScriptId(), + MelMWIcon(), + MelString(b'TEXT', u'book_text'), + MelMWEnchantment(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreBsgn(MelRecord): + """Birthsign.""" + rec_sig = b'BSGN' + + melSet = MelSet( + MelMWId(), + MelMWFull(), + MelGroups(u'birth_sign_spells', + MelString(b'NPCS', u'spell_id'), + ), + MelString(b'TNAM', u'texture_filename'), + MelDescription(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreCell(MelRecord): + """Cell.""" + rec_sig = b'CELL' + + _cell_flags = Flags(0, Flags.getNames( + (0, u'is_interior_cell'), + (1, u'has_water'), + (2, u'illegal_to_sleep_here'), + (7, u'behave_like_exterior'), + )) + + melSet = MelSet( + MelMWId(), + MelStruct(b'DATA', u'3I', (_cell_flags, u'cell_flags'), u'cell_x', + u'cell_y'), + MelString(b'RGNN', u'region_name'), + MelColorO(b'NAM5'), + MelOptFloat(b'WHGT', u'water_height'), + MelOptStruct(b'AMBI', u'12Bf', u'ambient_red', u'ambient_blue', + u'ambient_green', u'unused_alpha1', u'sunlight_red', + u'sunlight_blue', u'sunlight_green', u'unused_alpha2', u'fog_red', + u'fog_blue', u'fog_green', u'unused_alpha3'), + MelGroups(u'moved_references', + MelUInt32(b'MVRF', u'reference_id'), + MelString(b'CNAM', u'new_interior_cell'), + # None here are on purpose - only present for exterior cells, and + # zeroes are perfectly valid X/Y coordinates + ##: Double-check the signeds - UESP does not list them either way + MelOptStruct(b'CNDT', u'2i', (u'new_exterior_cell_x', None), + (u'new_exterior_cell_y', None)), + MelReference(), + ), + MelGroups(u'persistent_children', + MelReference(), + ), + MelCounter(MelUInt32(b'NAM0', u'temporary_children_counter'), + counts=u'temporary_children'), + MelGroups(u'temporary_children', + MelReference(), + ), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreClas(MelRecord): + """Class.""" + rec_sig = b'CLAS' + + _class_flags = Flags(0, Flags.getNames(u'class_playable')) + _ac_flags = Flags(0, Flags.getNames( + u'ac_weapon', + u'ac_armor', + u'ac_clothing', + u'ac_books', + u'ac_ingredients', + u'ac_picks', + u'ac_probes', + u'ac_lights', + u'ac_apparatus', + u'ac_repair_items', + u'ac_misc', + u'ac_spells', + u'ac_magic_items', + u'ac_potions', + u'ac_training', + u'ac_spellmaking', + u'ac_enchanting', + u'ac_repair', + )) + + melSet = MelSet( + MelMWId(), + MelMWFull(), + ##: UESP says 'alternating minor/major' skills - not sure what exactly + # it means, check with real data + MelStruct(b'CLDT', u'15I', u'primary1', u'primary2', + u'specialization', u'minor1', u'major1', u'minor2', u'major2', + u'minor3', u'major3', u'minor4', u'major4', u'minor5', u'major5', + (_class_flags, u'class_flags'), (_ac_flags, u'auto_calc_flags')), + MelDescription(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreClot(MelRecord): + """Clothing.""" + rec_sig = b'CLOT' + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelStruct(b'CTDT', u'If2H', u'clot_type', u'clot_weight', + u'clot_value', u'enchant_points'), + MelScriptId(), + MelMWIcon(), + MelArmorData(), + MelMWEnchantment(), + ) + __slots__ = melSet.getSlotsUsed() diff --git a/Mopy/bash/game/skyrim/records.py b/Mopy/bash/game/skyrim/records.py index 98a7bc8304..4d0bd95014 100644 --- a/Mopy/bash/game/skyrim/records.py +++ b/Mopy/bash/game/skyrim/records.py @@ -40,7 +40,8 @@ MreActorBase, MreWithItems, MelCtdaFo3, MelRef3D, MelXlod, \ MelWorldBounds, MelEnableParent, MelRefScale, MelMapMarker, MelMdob, \ MelEnchantment, MelDecalData, MelDescription, MelSInt16, MelSkipInterior, \ - MelPickupSound, MelDropSound, MelActivateParents, BipedFlags + MelPickupSound, MelDropSound, MelActivateParents, BipedFlags, MelColor, \ + MelColorO from ...exception import ModError, ModSizeError, StateError # Set MelModel in brec but only if unset, otherwise we are being imported from # fallout4.records @@ -163,19 +164,6 @@ def __init__(self): MelOptStruct.__init__(self,'COED','=IIf',(FID,'owner'),(FID,'glob'), 'itemCondition') -#------------------------------------------------------------------------------ -class MelColor(MelStruct): - """Required Color.""" - def __init__(self, signature='CNAM'): - MelStruct.__init__(self, signature, '=4B', 'red', 'green', 'blue', - 'unk_c') - -class MelColorO(MelOptStruct): - """Optional Color.""" - def __init__(self, signature='CNAM'): - MelOptStruct.__init__(self, signature, '=4B', 'red', 'green', 'blue', - 'unk_c') - #------------------------------------------------------------------------------ class MelConditions(MelGroups): """A list of conditions. See also MelConditionCounter, which is commonly @@ -241,6 +229,29 @@ class MelEquipmentType(MelOptFid): def __init__(self): super(MelEquipmentType, self).__init__(b'ETYP', u'equipment_type') +#------------------------------------------------------------------------------ +class MelIdleHandler(MelGroup): + """Occurs three times in PACK, so moved here to deduplicate the + definition a bit.""" + # The subrecord type used for the marker + _attr_lookup = { + u'on_begin': b'POBA', + u'on_change': b'POCA', + u'on_end': b'POEA', + } + + def __init__(self, attr): + super(MelIdleHandler, self).__init__(attr, + MelBase(self._attr_lookup[attr], attr + u'_marker'), + MelFid(b'INAM', u'idle_anim'), + # The next four are leftovers from earlier CK versions + MelBase(b'SCHR', u'unused1'), + MelBase(b'SCTX', u'unused2'), + MelBase(b'QNAM', u'unused3'), + MelBase(b'TNAM', u'unused4'), + MelTopicData(u'idle_topic_data'), + ) + #------------------------------------------------------------------------------ class MelItems(MelGroups): """Wraps MelGroups for the common task of defining a list of items.""" @@ -3746,28 +3757,6 @@ def __init__(self, attr): MelUInt32('PNAM', (self._DataInputFlags, 'input_flags', 0)), ), - class MelIdleHandler(MelGroup): - """Occurs three times in PACK, so moved here to deduplicate the - definition a bit.""" - # The subrecord type used for the marker - _attr_lookup = { - 'on_begin': 'POBA', - 'on_change': 'POCA', - 'on_end': 'POEA', - } - - def __init__(self, attr): - MelGroup.__init__(self, attr, - MelBase(self._attr_lookup[attr], attr + '_marker'), - MelFid('INAM', 'idle_anim'), - # The next four are leftovers from earlier CK versions - MelBase('SCHR', 'unused1'), - MelBase('SCTX', 'unused2'), - MelBase('QNAM', 'unused3'), - MelBase('TNAM', 'unused4'), - MelTopicData('idle_topic_data'), - ) - melSet = MelSet( MelEdid(), MelVmad(), @@ -3846,9 +3835,9 @@ def __init__(self, attr): ), ), MelDataInputs('data_inputs2'), - MelIdleHandler('on_begin'), - MelIdleHandler('on_end'), - MelIdleHandler('on_change'), + MelIdleHandler(u'on_begin'), + MelIdleHandler(u'on_end'), + MelIdleHandler(u'on_change'), ).with_distributor({ b'PKDT': { b'CTDA|CIS1|CIS2': u'conditions', From b602d19b851d96e6ff3c472848451b848305106b Mon Sep 17 00:00:00 2001 From: Infernio Date: Tue, 13 Oct 2020 15:17:20 +0200 Subject: [PATCH 3/9] TES3: Define CONT, CREA, DIAL, DOOR and ENCH TES3: Define CONT I extracted MelItems because I know we'll need it again (it's Morrowind's version of CNTO, used to define all inventories). Fix VTXT\LAND getting dumped incorrectly This is only a theoretical bug, should not actually impact anything. It was just annoying me that the AttrExistsDecider didn't actually do anything (always resolves to True since MelUnion default-sets all of its alternatives). TES3: Define CREA Also some refactoring on SPLO subrecords for the other games. TES3: Define DIAL TES3: Define DOOR TES3: Define ENCH Also some refactoring on existing definitions (YAGNI on those unused attr='effects' parameters). --- Mopy/bash/brec/advanced_elements.py | 18 ++- Mopy/bash/brec/common_records.py | 6 +- Mopy/bash/brec/common_subrecords.py | 9 +- Mopy/bash/brec/utils_constants.py | 1 + Mopy/bash/game/fallout3/records.py | 11 +- Mopy/bash/game/morrowind/records.py | 232 +++++++++++++++++++++++++--- Mopy/bash/game/oblivion/records.py | 10 +- Mopy/bash/game/skyrim/records.py | 13 +- 8 files changed, 246 insertions(+), 54 deletions(-) diff --git a/Mopy/bash/brec/advanced_elements.py b/Mopy/bash/brec/advanced_elements.py index 186b24e26e..e2a8fb9204 100644 --- a/Mopy/bash/brec/advanced_elements.py +++ b/Mopy/bash/brec/advanced_elements.py @@ -37,7 +37,7 @@ from .basic_elements import MelBase, MelNull, MelObject, MelStruct from .mod_io import ModWriter from .. import exception -from ..bolt import sio, struct_pack +from ..bolt import GPath, sio, struct_pack #------------------------------------------------------------------------------ class _MelDistributor(MelNull): @@ -570,18 +570,22 @@ def _decide_common(self, record): """Performs the actual decisions for both loading and dumping.""" raise exception.AbstractError() -class AttrExistsDecider(ACommonDecider): - """Decider that returns True if an attribute with the specified name is - present on the record.""" +class FidNotNullDecider(ACommonDecider): + """Decider that returns True if the FormID attribute with the specified + name is not NULL.""" def __init__(self, target_attr): - """Creates a new AttrExistsDecider with the specified attribute. + """Creates a new FidNotNullDecider with the specified attribute. :param target_attr: The name of the attribute to check. :type target_attr: unicode""" - self.target_attr = target_attr + self._target_attr = target_attr def _decide_common(self, record): - return hasattr(record, self.target_attr) + ##: Wasteful, but bush imports brec which uses this decider, so we + # can't import bush in __init__... + from .. import bush + return getattr(record, self._target_attr) != ( + GPath(bush.game.master_file), 0) class AttrValDecider(ACommonDecider): """Decider that returns an attribute value (may optionally apply a function diff --git a/Mopy/bash/brec/common_records.py b/Mopy/bash/brec/common_records.py index 565d8939f8..51bdae5bad 100644 --- a/Mopy/bash/brec/common_records.py +++ b/Mopy/bash/brec/common_records.py @@ -29,7 +29,7 @@ import struct from operator import attrgetter -from .advanced_elements import AttrExistsDecider, AttrValDecider, MelArray, \ +from .advanced_elements import FidNotNullDecider, AttrValDecider, MelArray, \ MelUnion from .basic_elements import MelBase, MelFid, MelFids, MelFloat, MelGroups, \ MelLString, MelNull, MelStruct, MelUInt32, MelSInt32 @@ -219,11 +219,11 @@ class MreLand(MelRecord): b'BTXT': MelStruct(b'BTXT', u'IBsh', (FID, u'btxt_texture'), u'quadrant', u'unknown', u'layer'), }), - # VTXT only exists for ATXT layers + # VTXT only exists for ATXT layers, i.e. if ATXT's FormID is valid MelUnion({ True: MelBase(b'VTXT', u'alpha_layer_data'), False: MelNull(b'VTXT'), - }, decider=AttrExistsDecider(u'atxt_texture')), + }, decider=FidNotNullDecider(u'atxt_texture')), ), MelArray('vertex_textures', MelFid('VTEX', 'vertex_texture'), diff --git a/Mopy/bash/brec/common_subrecords.py b/Mopy/bash/brec/common_subrecords.py index a6be545177..a4d19f501c 100644 --- a/Mopy/bash/brec/common_subrecords.py +++ b/Mopy/bash/brec/common_subrecords.py @@ -30,7 +30,8 @@ MelUnion, PartialLoadDecider, FlagDecider from .basic_elements import MelBase, MelFid, MelGroup, MelGroups, MelLString, \ MelNull, MelSequential, MelString, MelStruct, MelUInt32, MelOptStruct, \ - MelOptFloat, MelOptUInt8, MelOptUInt32, MelOptFid, MelReadOnly, MelUInt8 + MelOptFloat, MelOptUInt8, MelOptUInt32, MelOptFid, MelReadOnly, MelUInt8, \ + MelFids from .utils_constants import _int_unpacker, FID, null1, null2, null3, null4 from ..bolt import Flags, encode, struct_pack, struct_unpack @@ -598,6 +599,12 @@ class MelRefScale(MelOptFloat): def __init__(self): super(MelRefScale, self).__init__(b'XSCL', (u'ref_scale', 1.0)) +#------------------------------------------------------------------------------ +class MelSpells(MelFids): + """Handles the common SPLO subrecord.""" + def __init__(self): + super(MelSpells, self).__init__(b'SPLO', u'spells') + #------------------------------------------------------------------------------ class MelWorldBounds(MelSequential): """Worlspace (WRLD) bounds.""" diff --git a/Mopy/bash/brec/utils_constants.py b/Mopy/bash/brec/utils_constants.py index babb2a0734..757d93062e 100644 --- a/Mopy/bash/brec/utils_constants.py +++ b/Mopy/bash/brec/utils_constants.py @@ -110,6 +110,7 @@ def __init__(self, flag_default=0, new_flag_names=None): null2 = null1 * 2 null3 = null1 * 3 null4 = null1 * 4 +null32 = null1 * 32 # Hack for allowing record imports from parent games - set per game MelModel = None # type: type diff --git a/Mopy/bash/game/fallout3/records.py b/Mopy/bash/game/fallout3/records.py index 855eff02dd..086eab52f2 100644 --- a/Mopy/bash/game/fallout3/records.py +++ b/Mopy/bash/game/fallout3/records.py @@ -43,7 +43,7 @@ MelCtdaFo3, MelRef3D, MelXlod, MelNull, MelWorldBounds, MelEnableParent, \ MelRefScale, MelMapMarker, MelActionFlags, MelEnchantment, MelScript, \ MelDecalData, MelDescription, MelLists, MelPickupSound, MelDropSound, \ - MelActivateParents, BipedFlags + MelActivateParents, BipedFlags, MelSpells from ...exception import ModError, ModSizeError # Set MelModel in brec but only if unset if brec.MelModel is None: @@ -160,9 +160,8 @@ def __init__(self, attr=u'destructible'): #------------------------------------------------------------------------------ class MelEffects(MelGroups): """Represents ingredient/potion/enchantment/spell effects.""" - - def __init__(self, attr=u'effects'): - super(MelEffects, self).__init__(attr, + def __init__(self): + super(MelEffects, self).__init__(u'effects', MelFid(b'EFID', u'baseEffect'), MelStruct(b'EFIT', u'4Ii', u'magnitude', u'area', u'duration', u'recipient', u'actorValue'), @@ -928,7 +927,7 @@ class MreCrea(MreActor): MelBounds(), MelFull(), MelModel(), - MelFids('SPLO','spells'), + MelSpells(), MelEnchantment(), MelUInt16('EAMT', 'eamt'), MelStrings('NIFZ','bodyParts'), @@ -2034,7 +2033,7 @@ class MelNpcDnam(MelLists): MelEnchantment(), MelUInt16('EAMT', 'unarmedAttackAnimation'), MelDestructible(), - MelFids('SPLO','spells'), + MelSpells(), MelScript(), MelItems(), MelStruct('AIDT','=5B3sIbBbBi', ('aggression', 0), ('confidence',2), diff --git a/Mopy/bash/game/morrowind/records.py b/Mopy/bash/game/morrowind/records.py index 8364cb7af9..51fef41f9a 100644 --- a/Mopy/bash/game/morrowind/records.py +++ b/Mopy/bash/game/morrowind/records.py @@ -29,7 +29,7 @@ MreHeaderBase, MelUnion, SaveDecider, MelNull, MelSequential, MelRecord, \ MelGroup, MelGroups, MelUInt8, MelDescription, MelUInt32, MelColorO,\ MelOptStruct, MelCounter, MelRefScale, MelOptSInt32, MelRef3D, \ - MelOptFloat, MelOptUInt32, MelIcons + MelOptFloat, MelOptUInt32, MelIcons, MelFloat, null1, null3, null32 if brec.MelModel is None: class _MelModel(MelGroup): @@ -49,6 +49,38 @@ def _decode_raw(target_str): decode(x, avoidEncodings=(u'utf8', u'utf-8')) for x in cstrip(target_str).split(b'\n')) +#------------------------------------------------------------------------------ +class MelAIAccompanyPackage(MelOptStruct): + """Deduplicated from AI_E and AI_F (see below).""" + def __init__(self, ai_package_sig): + super(MelAIAccompanyPackage, self).__init__(ai_package_sig, + u'3fH32sBs', u'dest_x', u'dest_y', u'dest_z', u'package_duration', + (u'package_id', null32), (u'unknown_marker', 1), + (u'unused1', null1)) + +class MelAIPackages(MelGroups): + """Handles the AI_* and CNDT subrecords, which have the additional + complication that they may occur in any order.""" + def __init__(self): + super(MelAIPackages, self).__init__(u'aiPackages', + MelUnion({ + b'AI_A': MelStruct(b'AI_A', u'=32sB', + (u'package_name', null32), (u'unknown_marker', 1)), + b'AI_E': MelAIAccompanyPackage(b'AI_E'), + b'AI_F': MelAIAccompanyPackage(b'AI_F'), + b'AI_T': MelStruct(b'AI_T', u'3fB3s', u'dest_x', u'dest_y', + u'dest_z', (u'unknown_marker', 1), (u'unused1', null1)), + b'AI_W': MelStruct(b'AI_W', u'=2H10B', u'wanter_distance', + u'wanter_duration', u'time_of_day', u'idle_1', u'idle_2', + u'idle_3', u'idle_4', u'idle_5', u'idle_6', u'idle_7', + u'idle_8', (u'unknown_marker', 1)), + }), + # Only present for AI_E and AI_F, but should be fine since the + # default for MelString is None, so won't be dumped unless already + # present (i.e. the file is already broken) + MelString(b'CNDT', u'cell_name'), + ) + #------------------------------------------------------------------------------ class MelArmorData(MelGroups): """Handles the INDX, BNAM and CNAM subrecords shared by ARMO and CLOT.""" @@ -59,12 +91,47 @@ def __init__(self): MelString(b'CNAM', u'armor_name_female'), ) +#------------------------------------------------------------------------------ +class MelDestinations(MelGroups): + """Handles the common DODT/DNAM subrecords.""" + def __init__(self): + super(MelDestinations, self).__init__(u'cell_travel_destinations', + MelStruct(b'DODT', u'6f', u'dest_pos_x', u'dest_pos_y', + u'dest_pos_z', u'dest_rot_x', u'dest_rot_y', u'dest_rot_z'), + MelString(b'DNAM', u'dest_cell_name'), + ) + +#------------------------------------------------------------------------------ +class MelEffects(MelGroups): + """Handles the list of ENAM structs present on several records.""" + def __init__(self): + super(MelEffects, self).__init__(u'effects', + MelStruct(b'ENAM', u'H2b5I', u'effect_index', u'skill_affected', + u'attribute_affected', u'ench_range', u'ench_area', + u'ench_duration', u'ench_magnitude_min', + u'ench_magnitude_max'), + ) + +#------------------------------------------------------------------------------ +class MelItems(MelGroups): + """Wraps MelGroups for the common task of defining a list of items.""" + def __init__(self): + super(MelItems, self).__init__(u'items', + MelStruct(b'NPCO', u'I32s', u'count', (u'item', null32)), + ) + #------------------------------------------------------------------------------ class MelMWEnchantment(MelString): """Handles ENAM, Morrowind's version of EITM.""" def __init__(self): super(MelMWEnchantment, self).__init__(b'ENAM', u'enchantment') +#------------------------------------------------------------------------------ +class MelMWFull(MelString): + """Defines FNAM, Morrowind's version of FULL.""" + def __init__(self): + super(MelMWFull, self).__init__(b'FNAM', u'full') + #------------------------------------------------------------------------------ class MelMWIcon(MelIcons): """Handles the common ITEX record, Morrowind's version of ICON.""" @@ -78,10 +145,12 @@ def __init__(self): super(MelMWId, self).__init__(b'NAME', u'mw_id') #------------------------------------------------------------------------------ -class MelMWFull(MelString): - """Defines FNAM, Morrowind's version of FULL.""" +class MelMWSpells(MelGroups): + """Handles NPCS, Morrowind's version of SPLO.""" def __init__(self): - super(MelMWFull, self).__init__(b'FNAM', u'full') + super(MelMWSpells, self).__init__(u'spells', + MelStruct(b'NPCS', u'32s', (u'spell_id', null32)), + ) #------------------------------------------------------------------------------ class MelReference(MelSequential): @@ -103,12 +172,7 @@ def __init__(self): # object type MelBase(b'INTV', u'remaining_usage'), MelOptUInt32(b'NAM9', u'gold_value'), - MelGroups(u'cell_travel_destinations', - MelStruct(b'DODT', u'6f', u'dest_pos_x', u'dest_pos_y', - u'dest_pos_z', u'dest_rot_x', u'dest_rot_y', - u'dest_rot_z'), - MelString(b'DNAM', u'dest_cell_name'), - ), + MelDestinations(), MelOptUInt32(b'FLTV', u'lock_level'), MelString(b'KNAM', u'key_name'), MelString(b'TNAM', u'trap_name'), @@ -166,13 +230,13 @@ def dumpData(self, record, out): melSet = MelSet( MelTes3Hedr(b'HEDR', u'fI32s256sI', (u'version', 1.3), u'esp_flags', - u'author', u'description', u'numRecords'), + (u'author', null32), (u'description', null32 * 8), u'numRecords'), MreHeaderBase.MelMasterNames(), MelSavesOnly( # Wrye Mash calls unknown1 'day', but that seems incorrect? MelStruct(b'GMDT', u'6f64sf32s', u'pc_curr_health', - u'pc_max_health', u'unknown1', u'unknown2', u'unknown3', - u'unknown4', u'curr_cell', u'unknown5', u'pc_name'), + u'pc_max_health', u'unknown1', u'unknown2', u'unknown3', + u'unknown4', u'curr_cell', u'unknown5', (u'pc_name', null32)), MelBase(b'SCRD', u'unknown_scrd'), # likely screenshot-related MelArray(u'screenshot_data', # Yes, the correct order is bgra @@ -202,8 +266,6 @@ class MreAlch(MelRecord): """Potion.""" rec_sig = b'ALCH' - _potion_flags = Flags(0, Flags.getNames(u'auto_calc')) - melSet = MelSet( MelMWId(), MelModel(), @@ -211,13 +273,8 @@ class MreAlch(MelRecord): MelScriptId(), MelMWFull(), MelStruct(b'ALDT', u'f2I', u'potion_weight', u'potion_value', - (_potion_flags, u'potion_flags')), - MelGroups(u'potion_enchantments', - MelStruct(b'ENAM', u'H2b5I', u'effect_index', u'skill_affected', - u'attribute_affected', u'ench_range', u'ench_area', - u'ench_duration', u'ench_magnitude_min', - u'ench_magnitude_max'), - ), + u'potion_auto_calc'), + MelEffects(), ) __slots__ = melSet.getSlotsUsed() @@ -299,9 +356,7 @@ class MreBsgn(MelRecord): melSet = MelSet( MelMWId(), MelMWFull(), - MelGroups(u'birth_sign_spells', - MelString(b'NPCS', u'spell_id'), - ), + MelMWSpells(), MelString(b'TNAM', u'texture_filename'), MelDescription(), ) @@ -408,3 +463,130 @@ class MreClot(MelRecord): MelMWEnchantment(), ) __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreCont(MelRecord): + """Container.""" + rec_sig = b'CONT' + + _cont_flags = Flags(0, Flags.getNames( + u'cont_organic', + u'cont_respawns', + u'default_unknown', # always set + )) + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelFloat(b'CNDT', u'cont_weight'), + MelUInt32(b'FLAG', (_cont_flags, u'cont_flags')), + MelItems(), + MelScriptId(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreCrea(MelRecord): + """Creature.""" + rec_sig = b'CREA' + + _crea_flags = Flags(0, Flags.getNames( + u'biped', # names match those of MreCrea._flags in later games + u'respawn', + u'weaponAndShield', + u'crea_none', + u'swims', + u'flies', + u'walks', + u'default_flags', + u'essential', + u'skeleton_blood', + u'metal_blood', + )) + _ai_flags = Flags(0, Flags.getNames( + u'ai_weapon', + u'ai_armor', + u'ai_clothing', + u'ai_books', + u'ai_ingredient', + u'ai_picks', + u'ai_probes', + u'ai_lights', + u'ai_apparatus', + u'ai_repair_items', + u'ai_misc', + u'ai_spells', + u'ai_magic_items', + u'ai_potions', + u'ai_training', + u'ai_spellmaking', + u'ai_enchanting', + u'ai_repair', + )) + + melSet = MelSet( + MelMWId(), + MelModel(), + MelString(b'CNAM', u'sound_gen_creature'), + MelMWFull(), + MelScriptId(), + MelStruct(b'NPDT', u'24I', u'crea_type', u'crea_level', + u'crea_strength', u'crea_intelligence', u'crea_willpower', + u'crea_agility', u'crea_speed', u'crea_endurance', + u'crea_personality', u'crea_luck', u'crea_health', + u'crea_spell_points', u'crea_fatigue', u'crea_soul', + u'crea_combat', u'crea_magic', u'crea_stealth', + u'crea_attack_min_1', u'crea_attack_max_1', u'crea_attack_min_2', + u'crea_attack_max_2', u'crea_attack_min_3', u'crea_attack_max_3', + u'crea_gold'), + MelUInt32(b'FLAG', (_crea_flags, u'crea_flags')), + MelRefScale(), + MelItems(), + MelMWSpells(), + MelStruct(b'AIDT', u'Bs3B3sI', u'ai_hello', (u'unknown1', null1), + u'ai_fight', u'ai_flee', u'ai_alarm', (u'unknown2', null3), + (_ai_flags, u'ai_flags')), + MelDestinations(), + MelAIPackages(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreDial(MelRecord): + """Dialog Topic.""" + rec_sig = b'DIAL' + + melSet = MelSet( + MelMWId(), + MelUInt8(b'DATA', u'dialogue_type'), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreDoor(MelRecord): + """Door.""" + rec_sig = b'DOOR' + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelScriptId(), + MelString(b'SNAM', u'sound_open'), + MelString(b'ANAM', u'sound_close'), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreEnch(MelRecord): + """Enchantment.""" + rec_sig = b'ENCH' + + melSet = MelSet( + MelMWId(), + MelStruct(b'ENDT', u'4I', u'ench_type', u'ench_cost', u'ench_charge', + u'ench_auto_calc'), + MelEffects(), + ) + __slots__ = melSet.getSlotsUsed() diff --git a/Mopy/bash/game/oblivion/records.py b/Mopy/bash/game/oblivion/records.py index 5045a20b72..391fab3e77 100644 --- a/Mopy/bash/game/oblivion/records.py +++ b/Mopy/bash/game/oblivion/records.py @@ -38,7 +38,7 @@ MelArray, MelWthrColors, MelObject, MreActorBase, MreWithItems, \ MelReadOnly, MelCtda, MelRef3D, MelXlod, MelWorldBounds, MelEnableParent, \ MelRefScale, MelMapMarker, MelActionFlags, MelPartialCounter, MelScript, \ - MelDescription, BipedFlags + MelDescription, BipedFlags, MelSpells # Set brec MelModel to the one for Oblivion if brec.MelModel is None: @@ -627,7 +627,7 @@ class MreBsgn(MelRecord): MelFull(), MelIcon(), MelDescription(u'text'), - MelFids('SPLO','spells'), + MelSpells(), ) __slots__ = melSet.getSlotsUsed() @@ -812,7 +812,7 @@ class MreCrea(MreActorBase): MelEdid(), MelFull(), MelModel(), - MelFids('SPLO','spells'), + MelSpells(), MelStrings('NIFZ','bodyParts'), MelBase('NIFT','nift_p'), # Texture File Hashes MelStruct('ACBS','=I3Hh2H', @@ -1391,7 +1391,7 @@ class MelNpcData(MelLists): ), MelFid('INAM','deathItem'), MelFid('RNAM','race'), - MelFids('SPLO','spells'), + MelSpells(), MelScript(), MelItems(), MelStruct('AIDT', '=4BIbB2s', ('aggression', 5), ('confidence', 50), @@ -1554,7 +1554,7 @@ class MreRace(MelRecord): MelEdid(), MelFull(), MelDescription(u'text'), - MelFids('SPLO','spells'), + MelSpells(), MelGroups('relations', MelStruct('XNAM', 'Ii', (FID, 'faction'), 'mod'), ), diff --git a/Mopy/bash/game/skyrim/records.py b/Mopy/bash/game/skyrim/records.py index 4d0bd95014..a1d60cd0b7 100644 --- a/Mopy/bash/game/skyrim/records.py +++ b/Mopy/bash/game/skyrim/records.py @@ -41,7 +41,7 @@ MelWorldBounds, MelEnableParent, MelRefScale, MelMapMarker, MelMdob, \ MelEnchantment, MelDecalData, MelDescription, MelSInt16, MelSkipInterior, \ MelPickupSound, MelDropSound, MelActivateParents, BipedFlags, MelColor, \ - MelColorO + MelColorO, MelSpells from ...exception import ModError, ModSizeError, StateError # Set MelModel in brec but only if unset, otherwise we are being imported from # fallout4.records @@ -215,11 +215,10 @@ def __init__(self,attr='destructible'): #------------------------------------------------------------------------------ class MelEffects(MelGroups): """Represents ingredient/potion/enchantment/spell effects.""" - - def __init__(self,attr='effects'): - MelGroups.__init__(self,attr, - MelFid('EFID','name'), # baseEffect, name - MelStruct('EFIT','f2I','magnitude','area','duration',), + def __init__(self): + MelGroups.__init__(self, u'effects', + MelFid(b'EFID', u'name'), # baseEffect, name + MelStruct(b'EFIT', u'f2I', u'magnitude', u'area', u'duration'), MelConditions(), ) @@ -3609,7 +3608,7 @@ class MreNpc(MreActorBase): MelOptFid('TPLT', 'template'), MelFid('RNAM','race'), MelCounter(MelUInt32(b'SPCT', u'spell_count'), counts=u'spells'), - MelFids('SPLO', 'spells'), + MelSpells(), MelDestructible(), MelOptFid('WNAM', 'wornArmor'), MelOptFid('ANAM', 'farawaymodel'), From a1249b73428a896aa9afe8032de63876d3a73a1c Mon Sep 17 00:00:00 2001 From: Infernio Date: Thu, 15 Oct 2020 22:44:39 +0200 Subject: [PATCH 4/9] TES3: Define FACT, GLOB, GMST, INFO and INGR TES3: Define FACT Ugh, just look at that FADT subrecord. Why... TES3: Define GLOB Took this opportunity to finally create MelFixedString. Still need a better solution for fixed-length strings in the middle of structs. Note also the renames on MreGlob - scary, but much better names (unique, for a start). Interlude: Rewrite fixed-length string handling Completely untested, of course, but should be much better. No idea how this intersects/conflicts with 480-pt3 yet. Still, takes the awful MelTes3Hedr hack with it. TES3: Define GMST Note I also dropped the useless decode() calls in MreGmst - EDIDs are decoded no matter what, so no point in these. Plus rename from eid -> e for these temp vars, eid should be reserved for the actual EDID subrecord, thanks. TES3: Define INFO This won't be fun to handle when we get to writing - it has the same PNAM bullshit, but now it's a full doubly-linked leveled list. Except not really of course, since mods can and will violate the LL structure. So sorting will get even more complicated now. Yay. TES3: Define INGR --- Mopy/bash/bosh/save_headers.py | 7 +- Mopy/bash/brec/advanced_elements.py | 2 +- Mopy/bash/brec/basic_elements.py | 21 +- Mopy/bash/brec/common_records.py | 9 +- Mopy/bash/brec/utils_constants.py | 34 +++- Mopy/bash/game/morrowind/records.py | 192 ++++++++++++++---- Mopy/bash/game/skyrim/records.py | 5 +- .../patcher/patchers/multitweak_settings.py | 4 +- 8 files changed, 211 insertions(+), 63 deletions(-) diff --git a/Mopy/bash/bosh/save_headers.py b/Mopy/bash/bosh/save_headers.py index a700307f7b..4f64153355 100644 --- a/Mopy/bash/bosh/save_headers.py +++ b/Mopy/bash/bosh/save_headers.py @@ -32,7 +32,6 @@ __author__ = u'Utumno' import copy -import itertools import lz4.block import StringIO import struct @@ -611,11 +610,9 @@ def load_header(self, ins, load_image=False): save_info = ModInfo(self._save_path, load_cache=True) ##: Figure out where some more of these are (e.g. level) self.header_size = save_info.header.size - self.pcName = decode(cstrip(save_info.header.pc_name)) + self.pcName = save_info.header.pc_name self.pcLevel = 0 - self.pcLocation = decode(cstrip(save_info.header.curr_cell), - bolt.pluginEncoding, - avoidEncodings=(u'utf8', u'utf-8')) + self.pcLocation = save_info.header.curr_cell self.gameDays = self.gameTicks = 0 self.masters = save_info.masterNames[:] self.pc_curr_health = save_info.header.pc_curr_health diff --git a/Mopy/bash/brec/advanced_elements.py b/Mopy/bash/brec/advanced_elements.py index e2a8fb9204..7cd5d14265 100644 --- a/Mopy/bash/brec/advanced_elements.py +++ b/Mopy/bash/brec/advanced_elements.py @@ -719,7 +719,7 @@ class MelUnion(MelBase): u'f': MelFloat(b'DATA', u'value'), u's': MelLString(b'DATA', u'value'), }, decider=AttrValDecider( - u'eid', transformer=lambda eid: decode(eid[0]) if eid else u'i'), + u'eid', transformer=lambda e: e[0] if e else u'i'), fallback=MelSInt32(b'DATA', u'value') ), When a DATA subrecord is encountered, the union is asked to load it. It diff --git a/Mopy/bash/brec/basic_elements.py b/Mopy/bash/brec/basic_elements.py index a03cd81ec9..b9bdaae1d1 100644 --- a/Mopy/bash/brec/basic_elements.py +++ b/Mopy/bash/brec/basic_elements.py @@ -26,7 +26,7 @@ from __future__ import division, print_function import struct -from .utils_constants import FID, null1, _make_hashable +from .utils_constants import FID, null1, _make_hashable, FixedString from .. import bolt, exception from ..bolt import decode, encode @@ -487,9 +487,10 @@ def static_size(self): class MelString(MelBase): """Represents a mod record string element.""" - def __init__(self, subType, attr, default=None, maxSize=0): + def __init__(self, subType, attr, default=None, maxSize=0, minSize=0): super(MelString, self).__init__(subType, attr, default) self.maxSize = maxSize + self.minSize = minSize def loadData(self, record, ins, sub_type, size_, readId): value = ins.readString(size_, readId) @@ -498,7 +499,8 @@ def loadData(self, record, ins, sub_type, size_, readId): def dumpData(self,record,out): string_val = record.__getattribute__(self.attr) if string_val is not None: - out.write_string(self.subType, string_val, max_size=self.maxSize) + out.write_string(self.subType, string_val, max_size=self.maxSize, + min_size=self.minSize) #------------------------------------------------------------------------------ class MelUnicode(MelString): @@ -590,7 +592,10 @@ def dumpData(self,record,out): getter = record.__getattribute__ for attr,action in zip(self.attrs,self.actions): value = getter(attr) - if action: value = value.dump() + # Just in case, apply the action to itself before dumping to handle + # e.g. a FixedString getting assigned a unicode value. Worst case, + # this is just a noop. + if action: value = action(value).dump() valuesAppend(value) out.packSub(self.subType, self.struct_format, *values) @@ -605,6 +610,14 @@ def mapFids(self,record,function,save=False): def static_size(self): return struct.calcsize(self.struct_format) +#------------------------------------------------------------------------------ +class MelFixedString(MelStruct): + """Subrecord that stores a string of a constant length. Just a wrapper + around a struct with a single FixedString element.""" + def __init__(self, signature, attr, str_length, default=b''): + super(MelFixedString, self).__init__(signature, u'%us' % str_length, + (FixedString(str_length, default), attr)) + #------------------------------------------------------------------------------ # Simple primitive type wrappers class _MelSimpleStruct(MelStruct): diff --git a/Mopy/bash/brec/common_records.py b/Mopy/bash/brec/common_records.py index 51bdae5bad..a9e595d978 100644 --- a/Mopy/bash/brec/common_records.py +++ b/Mopy/bash/brec/common_records.py @@ -32,7 +32,7 @@ from .advanced_elements import FidNotNullDecider, AttrValDecider, MelArray, \ MelUnion from .basic_elements import MelBase, MelFid, MelFids, MelFloat, MelGroups, \ - MelLString, MelNull, MelStruct, MelUInt32, MelSInt32 + MelLString, MelNull, MelStruct, MelUInt32, MelSInt32, MelFixedString from .common_subrecords import MelEdid from .record_structs import MelRecord, MelSet from .utils_constants import FID @@ -174,10 +174,11 @@ class MreGlob(MelRecord): (short,long,float), are stored as floats -- which means that very large integers lose precision.""" rec_sig = b'GLOB' + melSet = MelSet( MelEdid(), - MelStruct('FNAM','s',('format','s')), - MelFloat('FLTV', 'value'), + MelFixedString(b'FNAM', u'global_format', 1, u's'), + MelFloat(b'FLTV', u'global_value'), ) __slots__ = melSet.getSlotsUsed() @@ -195,7 +196,7 @@ class MreGmstBase(MelRecord): u'f': MelFloat(b'DATA', u'value'), u's': MelLString(b'DATA', u'value'), }, decider=AttrValDecider( - u'eid', transformer=lambda eid: decode(eid[0]) if eid else u'i'), + u'eid', transformer=lambda e: e[0] if e else u'i'), fallback=MelSInt32(b'DATA', u'value') ), ) diff --git a/Mopy/bash/brec/utils_constants.py b/Mopy/bash/brec/utils_constants.py index 757d93062e..ee8eb79fd2 100644 --- a/Mopy/bash/brec/utils_constants.py +++ b/Mopy/bash/brec/utils_constants.py @@ -26,7 +26,8 @@ from __future__ import division, print_function import struct -from ..bolt import decode, Flags, struct_pack, struct_unpack +from .. import bolt +from ..bolt import cstrip, decode, Flags, struct_pack, struct_unpack # no local imports, imported everywhere in brec # Random stuff ---------------------------------------------------------------- @@ -66,6 +67,36 @@ def _make_hashable(target_obj): return tuple([_make_hashable(x) for x in target_obj]) return target_obj +class FixedString(unicode): + """An action for MelStructs that will decode and encode a fixed-length + string. Note that you do not need to specify defaults when using this.""" + __slots__ = (u'str_length',) + _str_encoding = bolt.pluginEncoding + + def __new__(cls, str_length, target_str=b''): + if isinstance(target_str, unicode): + decoded_str = target_str + else: + decoded_str = u'\n'.join( + decode(x, cls._str_encoding, + avoidEncodings=(u'utf8', u'utf-8')) + for x in cstrip(target_str).split(b'\n')) + new_str = super(FixedString, cls).__new__(cls, decoded_str) + new_str.str_length = str_length + return new_str + + def __call__(self, new_str): + # 0 is the default, so replace it with whatever we currently have + return FixedString(self.str_length, new_str or unicode(self)) + + def dump(self): + return bolt.encode_complex_string(self, max_size=self.str_length, + min_size=self.str_length) + +class AutoFixedString(FixedString): + """Variant of FixedString that uses chardet to detect encodings.""" + _str_encoding = None + # Reference (fid) ------------------------------------------------------------- def strFid(form_id): """Return a string representation of the fid.""" @@ -110,7 +141,6 @@ def __init__(self, flag_default=0, new_flag_names=None): null2 = null1 * 2 null3 = null1 * 3 null4 = null1 * 4 -null32 = null1 * 32 # Hack for allowing record imports from parent games - set per game MelModel = None # type: type diff --git a/Mopy/bash/game/morrowind/records.py b/Mopy/bash/game/morrowind/records.py index 51fef41f9a..0309d91c50 100644 --- a/Mopy/bash/game/morrowind/records.py +++ b/Mopy/bash/game/morrowind/records.py @@ -23,13 +23,14 @@ """This module contains the Morrowind record classes. Also contains records and subrecords used for the saves - see MorrowindSaveHeader for more information.""" -from ... import bolt, brec -from ...bolt import cstrip, decode, Flags +from ... import brec +from ...bolt import Flags from ...brec import MelBase, MelSet, MelString, MelStruct, MelArray, \ MreHeaderBase, MelUnion, SaveDecider, MelNull, MelSequential, MelRecord, \ MelGroup, MelGroups, MelUInt8, MelDescription, MelUInt32, MelColorO,\ MelOptStruct, MelCounter, MelRefScale, MelOptSInt32, MelRef3D, \ - MelOptFloat, MelOptUInt32, MelIcons, MelFloat, null1, null3, null32 + MelOptFloat, MelOptUInt32, MelIcons, MelFloat, null1, null3, MelSInt32, \ + MelFixedString, FixedString, AutoFixedString, MreGmstBase, MelOptUInt8 if brec.MelModel is None: class _MelModel(MelGroup): @@ -42,20 +43,13 @@ def __init__(self): #------------------------------------------------------------------------------ # Utilities ------------------------------------------------------------------- -#------------------------------------------------------------------------------ -def _decode_raw(target_str): - """Adapted from MelUnicode.loadData. ##: maybe move to bolt/brec?""" - return u'\n'.join( - decode(x, avoidEncodings=(u'utf8', u'utf-8')) for x - in cstrip(target_str).split(b'\n')) - #------------------------------------------------------------------------------ class MelAIAccompanyPackage(MelOptStruct): """Deduplicated from AI_E and AI_F (see below).""" def __init__(self, ai_package_sig): super(MelAIAccompanyPackage, self).__init__(ai_package_sig, u'3fH32sBs', u'dest_x', u'dest_y', u'dest_z', u'package_duration', - (u'package_id', null32), (u'unknown_marker', 1), + (FixedString(32), u'package_id'), (u'unknown_marker', 1), (u'unused1', null1)) class MelAIPackages(MelGroups): @@ -65,7 +59,8 @@ def __init__(self): super(MelAIPackages, self).__init__(u'aiPackages', MelUnion({ b'AI_A': MelStruct(b'AI_A', u'=32sB', - (u'package_name', null32), (u'unknown_marker', 1)), + (FixedString(32), u'package_name'), + (u'unknown_marker', 1)), b'AI_E': MelAIAccompanyPackage(b'AI_E'), b'AI_F': MelAIAccompanyPackage(b'AI_F'), b'AI_T': MelStruct(b'AI_T', u'3fB3s', u'dest_x', u'dest_y', @@ -117,7 +112,7 @@ class MelItems(MelGroups): """Wraps MelGroups for the common task of defining a list of items.""" def __init__(self): super(MelItems, self).__init__(u'items', - MelStruct(b'NPCO', u'I32s', u'count', (u'item', null32)), + MelStruct(b'NPCO', u'I32s', u'count', (FixedString(32), u'item')), ) #------------------------------------------------------------------------------ @@ -149,7 +144,7 @@ class MelMWSpells(MelGroups): """Handles NPCS, Morrowind's version of SPLO.""" def __init__(self): super(MelMWSpells, self).__init__(u'spells', - MelStruct(b'NPCS', u'32s', (u'spell_id', null32)), + MelFixedString(b'NPCS', u'spell_id', 32), ) #------------------------------------------------------------------------------ @@ -203,40 +198,17 @@ class MreTes3(MreHeaderBase): """TES3 Record. File header.""" rec_sig = b'TES3' - class MelTes3Hedr(MelStruct): - """Wrapper around MelStruct to handle the author and description - fields, which are padded to 32 and 256 bytes, respectively, with null - bytes.""" - def loadData(self, record, ins, sub_type, size_, readId): - super(MreTes3.MelTes3Hedr, self).loadData(record, ins, sub_type, - size_, readId) - # Strip off the null bytes and convert to unicode - record.author = _decode_raw(record.author) - record.description = _decode_raw(record.description) - - def dumpData(self, record, out): - # Store the original values in case we dump more than once - orig_author = record.author - orig_desc = record.description - # Encode and enforce limits, then dump out - record.author = bolt.encode_complex_string( - record.author, max_size=32, min_size=32) - record.description = bolt.encode_complex_string( - record.description, max_size=256, min_size=256) - super(MreTes3.MelTes3Hedr, self).dumpData(record, out) - # Restore the original values again, see comment above - record.author = orig_author - record.description = orig_desc - melSet = MelSet( - MelTes3Hedr(b'HEDR', u'fI32s256sI', (u'version', 1.3), u'esp_flags', - (u'author', null32), (u'description', null32 * 8), u'numRecords'), + MelStruct(b'HEDR', u'fI32s256sI', (u'version', 1.3), u'esp_flags', + (AutoFixedString(32), u'author'), + (AutoFixedString(256), u'description'), u'numRecords'), MreHeaderBase.MelMasterNames(), MelSavesOnly( # Wrye Mash calls unknown1 'day', but that seems incorrect? MelStruct(b'GMDT', u'6f64sf32s', u'pc_curr_health', u'pc_max_health', u'unknown1', u'unknown2', u'unknown3', - u'unknown4', u'curr_cell', u'unknown5', (u'pc_name', null32)), + u'unknown4', (FixedString(64), u'curr_cell'), u'unknown5', + (AutoFixedString(32), u'pc_name')), MelBase(b'SCRD', u'unknown_scrd'), # likely screenshot-related MelArray(u'screenshot_data', # Yes, the correct order is bgra @@ -590,3 +562,139 @@ class MreEnch(MelRecord): MelEffects(), ) __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreFact(MelRecord): + """Faction.""" + rec_sig = b'FACT' + + melSet = MelSet( + MelMWId(), + MelMWFull(), + MelGroups(u'ranks', # always 10 + MelString(b'RNAM', u'rank_name'), + ), + ##: Double-check that these are all unsigned (especially + # rank_*_reaction), xEdit makes most of them signed (and puts them in + # an enum, which makes no sense). Also, why couldn't Bethesda put these + # into the ranks list up above? + MelStruct(b'FADT', u'52I7iI', u'faction_attribute_1', + u'faction_attribute_2', u'rank_1_attribute_1', + u'rank_1_attribute_2', u'rank_1_skill_1', u'rank_1_skill_2', + u'rank_1_reaction', u'rank_2_attribute_1', u'rank_2_attribute_2', + u'rank_2_skill_1', u'rank_2_skill_2', u'rank_2_reaction', + u'rank_3_attribute_1', u'rank_3_attribute_2', u'rank_3_skill_1', + u'rank_3_skill_2', u'rank_3_reaction', u'rank_4_attribute_1', + u'rank_4_attribute_2', u'rank_4_skill_1', u'rank_4_skill_2', + u'rank_4_reaction', u'rank_5_attribute_1', u'rank_5_attribute_2', + u'rank_5_skill_1', u'rank_5_skill_2', u'rank_5_reaction', + u'rank_6_attribute_1', u'rank_6_attribute_2', u'rank_6_skill_1', + u'rank_6_skill_2', u'rank_6_reaction', u'rank_7_attribute_1', + u'rank_7_attribute_2', u'rank_7_skill_1', u'rank_7_skill_2', + u'rank_7_reaction', u'rank_8_attribute_1', u'rank_8_attribute_2', + u'rank_8_skill_1', u'rank_8_skill_2', u'rank_8_reaction', + u'rank_9_attribute_1', u'rank_9_attribute_2', u'rank_9_skill_1', + u'rank_9_skill_2', u'rank_9_reaction', u'rank_10_attribute_1', + u'rank_10_attribute_2', u'rank_10_skill_1', u'rank_10_skill_2', + u'rank_10_reaction', u'skill_1', u'skill_2', u'skill_3', + u'skill_4', u'skill_5', u'skill_6', u'skill_7', + u'hidden_from_pc'), + MelGroups(u'relations', # bad names to match other games + MelString(b'ANAM', u'faction'), + MelSInt32(b'INTV', u'mod'), + ), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreGlob(MelRecord): + """Global.""" + rec_sig = b'GLOB' + + melSet = MelSet( + MelMWId(), + MelFixedString(b'FNAM', u'global_format', 1, u's'), + MelFloat(b'FLTV', u'global_value'), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MelGmstUnion(MelUnion): + """Some GMSTs do not have one of the value subrecords - fall back to + using the first letter of the NAME subrecord in those cases.""" + _fmt_mapping = { + u'f': b'FLTV', + u'i': b'INTV', + u's': b'STRV', + } + + def _get_element_from_record(self, record): + if not hasattr(record, self.decider_result_attr): + format_char = record.mw_id[0] if record.mw_id else u'i' + return self._get_element(self._fmt_mapping[format_char]) + return super(MelGmstUnion, self)._get_element_from_record(record) + +class MreGmst(MreGmstBase): + """Game Setting.""" + melSet = MelSet( + MelMWId(), + MelGmstUnion({ + b'FLTV': MelFloat(b'FLTV', u'value'), + b'INTV': MelSInt32(b'INTV', u'value'), + b'STRV': MelString(b'STRV', u'value'), + }), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreInfo(MelRecord): + """Dialog Response.""" + rec_sig = b'INFO' + + melSet = MelSet( + MelString(b'INAM', u'info_name_string'), + MelString(b'PNAM', u'prev_info_name'), + MelString(b'NNAM', u'next_info_name'), + MelStruct(b'DATA', u'B3sIBbBs', u'dialogue_type', (u'unused1', null3), + u'disposition', u'dialogue_rank', u'speaker_gender', u'pc_rank', + (u'unused2', null1)), + MelString(b'ONAM', u'actor_name'), + MelString(b'RNAM', u'race_name'), + MelString(b'CNAM', u'class_name'), + MelString(b'FNAM', u'faction_name'), + MelString(b'ANAM', u'cell_name'), + MelString(b'DNAM', u'pc_faction_name'), + MelString(b'SNAM', u'sound_name'), + MelMWId(), + MelGroups(u'conditions', + MelString(b'SCVR', u'condition_string'), + # None here are on purpose - 0 is a valid value, but only certain + # conditions need these subrecords to be present + MelOptUInt32(b'INTV', (u'comparison_int', None)), + MelOptFloat(b'FLTV', (u'comparison_float', None)), + ), + MelString(b'BNAM', u'result_text'), + MelOptUInt8(b'QSTN', u'quest_name'), + MelOptUInt8(b'QSTF', u'quest_finished'), + MelOptUInt8(b'QSTR', u'quest_restart'), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreIngr(MelRecord): + """Ingredient.""" + rec_sig = b'INGR' + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelStruct(b'IRDT', u'fI12i', u'ingr_weight', u'ingr_value', + u'effect_index_1', u'effect_index_2', u'effect_index_3', + u'effect_index_4', u'skill_id_1', u'skill_id_2', u'skill_id_3', + u'skill_id_4', u'attribute_id_1', u'attribute_id_2', + u'attribute_id_3', u'attribute_id_4'), + MelScriptId(), + MelMWIcon(), + ) + __slots__ = melSet.getSlotsUsed() diff --git a/Mopy/bash/game/skyrim/records.py b/Mopy/bash/game/skyrim/records.py index a1d60cd0b7..009deaae35 100644 --- a/Mopy/bash/game/skyrim/records.py +++ b/Mopy/bash/game/skyrim/records.py @@ -41,7 +41,7 @@ MelWorldBounds, MelEnableParent, MelRefScale, MelMapMarker, MelMdob, \ MelEnchantment, MelDecalData, MelDescription, MelSInt16, MelSkipInterior, \ MelPickupSound, MelDropSound, MelActivateParents, BipedFlags, MelColor, \ - MelColorO, MelSpells + MelColorO, MelSpells, MelFixedString from ...exception import ModError, ModSizeError, StateError # Set MelModel in brec but only if unset, otherwise we are being imported from # fallout4.records @@ -2004,8 +2004,7 @@ class MreDial(MelRecord): MelFid('QNAM','quest',), MelStruct('DATA','2BH',(DialTopicFlags,'flags_dt',0),'category', 'subtype',), - # SNAM is a 4 byte string no length byte - TODO(inf) MelFixedString? - MelStruct('SNAM', '4s', ('subtypeName', null4)), + MelFixedString(b'SNAM', u'subtypeName', 4), MelUInt32(b'TIFC', u'info_count'), # Updated in MobDial.dump ) __slots__ = melSet.getSlotsUsed() diff --git a/Mopy/bash/patcher/patchers/multitweak_settings.py b/Mopy/bash/patcher/patchers/multitweak_settings.py index 8aa7686e6a..e1506a762b 100644 --- a/Mopy/bash/patcher/patchers/multitweak_settings.py +++ b/Mopy/bash/patcher/patchers/multitweak_settings.py @@ -44,10 +44,10 @@ def chosen_value(self): def wants_record(self, record): return (getattr(record, u'eid', None) and # skip missing and empty EDID record.eid.lower() == self.tweak_key and - record.value != self.chosen_value) + record.global_value != self.chosen_value) def tweak_record(self, record): - record.value = self.chosen_value + record.global_value = self.chosen_value def tweak_log(self, log, count): if count: log(u'* ' + _(u'%s set to: %4.2f') % ( From fe254ea9446c4917a1b5e6efaad6153bd26115be Mon Sep 17 00:00:00 2001 From: Infernio Date: Fri, 16 Oct 2020 23:51:18 +0200 Subject: [PATCH 5/9] TES3: Define LAND, LEVC, LEVI, LIGH and LOCK TES3: Define LAND Flags are not used/set in xEdit, and there's the question of what we'll do with a record that does not have a NAME subrecord (that's like a record without a FormID in Oblivion+). TES3: Define LEVC TES3: Define LEVI TES3: Define LIGH TES3: Define LOCK --- Mopy/bash/game/morrowind/__init__.py | 7 +- Mopy/bash/game/morrowind/records.py | 108 ++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/Mopy/bash/game/morrowind/__init__.py b/Mopy/bash/game/morrowind/__init__.py index 02dcf42cef..320434c3c4 100644 --- a/Mopy/bash/game/morrowind/__init__.py +++ b/Mopy/bash/game/morrowind/__init__.py @@ -97,10 +97,11 @@ class Bain(GameInfo.Bain): wrye_bash_data_dirs = GameInfo.Bain.wrye_bash_data_dirs | {u'Mash'} class Esp(GameInfo.Esp): - validHeaderVersions = (1.2, 1.3) - stringsFiles = [] - plugin_header_sig = b'TES3' check_master_sizes = True + max_lvl_list_size = 2 ** 32 - 1 + plugin_header_sig = b'TES3' + stringsFiles = [] + validHeaderVersions = (1.2, 1.3) @classmethod def init(cls): diff --git a/Mopy/bash/game/morrowind/records.py b/Mopy/bash/game/morrowind/records.py index 0309d91c50..09632ada90 100644 --- a/Mopy/bash/game/morrowind/records.py +++ b/Mopy/bash/game/morrowind/records.py @@ -30,7 +30,8 @@ MelGroup, MelGroups, MelUInt8, MelDescription, MelUInt32, MelColorO,\ MelOptStruct, MelCounter, MelRefScale, MelOptSInt32, MelRef3D, \ MelOptFloat, MelOptUInt32, MelIcons, MelFloat, null1, null3, MelSInt32, \ - MelFixedString, FixedString, AutoFixedString, MreGmstBase, MelOptUInt8 + MelFixedString, FixedString, AutoFixedString, MreGmstBase, MelOptUInt8, \ + MreLeveledListBase, MelUInt16 if brec.MelModel is None: class _MelModel(MelGroup): @@ -191,6 +192,29 @@ class MelScriptId(MelString): def __init__(self): super(MelScriptId, self).__init__(b'SCRI', u'script_id'), +#------------------------------------------------------------------------------ +class MreLeveledList(MreLeveledListBase): + """Base class for LEVC and LEVI.""" + _lvl_flags = Flags(0, Flags.getNames( + u'calcFromAllLevels', + u'calcForEachItem', # LEVI only, but will be ignored for LEVC so fine + )) + top_copy_attrs = (u'chanceNone',) + entry_copy_attrs = (u'listId', u'level') # no count + + # Bad names to mirror the other games (needed by MreLeveledListBase) + melSet = MelSet( + MelMWId(), + MelUInt32(b'DATA', (_lvl_flags, u'flags')), + MelUInt8(b'NNAM', u'chanceNone'), + MelCounter(MelUInt32(b'INDX', u'entry_count'), counts=u'entries'), + MelGroups(u'entries', + MelString(b'INAM', u'listId'), + MelUInt16(b'INTV', u'level'), + ), + ) + __slots__ = melSet.getSlotsUsed() + #------------------------------------------------------------------------------ # Shared (plugins + saves) record classes ------------------------------------- #------------------------------------------------------------------------------ @@ -698,3 +722,85 @@ class MreIngr(MelRecord): MelMWIcon(), ) __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreLand(MelRecord): + """Landscape.""" + rec_sig = b'LAND' + + _data_type_flags = Flags(0, Flags.getNames( ##: Shouldn't we set/use these? + u'include_vnml_vhgt_wnam', + u'include_vclr', + u'include_vtex', + )) + + ##: No MelMWId, will that be a problem? + melSet = MelSet( + MelStruct(b'INTV', u'2I', u'land_x', u'land_y'), + MelUInt32(b'DATA', (_data_type_flags, u'dt_flags')), + # These are all very large and too complex to manipulate -> MelBase + MelBase(b'VNML', u'vertex_normals'), + MelBase(b'VHGT', u'vertex_height_map'), + MelBase(b'WNAM', u'world_map_heights'), + MelBase(b'VCLR', u'vertex_colors'), + MelBase(b'VTEX', u'vertex_textures'), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreLevc(MreLeveledList): + """Leveled Creature.""" + rec_sig = b'LEVC' + __slots__ = [] + +#------------------------------------------------------------------------------ +class MreLevi(MreLeveledList): + """Leveled Item.""" + rec_sig = b'LEVI' + __slots__ = [] + +#------------------------------------------------------------------------------ +class MreLigh(MelRecord): + """Light.""" + rec_sig = b'LIGH' + + _light_flags = Flags(0, Flags.getNames( + u'dynamic', # Bad names to match the other games (for tweaks) + u'canTake', + u'negative', + u'flickers', + u'light_fire', + u'offByDefault', + u'flickerSlow', + u'pulse', + u'pulseSlow', + )) + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelMWIcon(), + MelStruct(b'LHDT', u'fIiI4BI', u'light_weight', u'light_value', + u'light_time', u'light_red', u'light_green', u'light_blue', + u'unused_alpha', (_light_flags, u'flags')), + MelString(b'SNAM', u'sound_name'), + MelScriptId(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreLock(MelRecord): + """Lockpicking Item.""" + rec_sig = b'LOCK' + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelStruct(b'LKDT', u'fIfI', u'lock_weight', u'lock_value', + u'lock_quality', u'lock_uses'), + MelScriptId(), + MelMWIcon(), + ) + __slots__ = melSet.getSlotsUsed() From dc89f190cd067713bed4a0fd509e556de0ef32a4 Mon Sep 17 00:00:00 2001 From: Infernio Date: Sat, 17 Oct 2020 16:22:00 +0200 Subject: [PATCH 6/9] TES3: Define LTEX, MGEF, MISC, NPC_ and PGRD TES3: Define LTEX TES3: Define MGEF TES3: Define MISC Add some more startup validation to records Namely ensuring that signatures are valid (4 bytes) and we never accidentally pad out structs. TES3: Define NPC_ TES3: Define PGRD --- Mopy/bash/brec/basic_elements.py | 10 ++ Mopy/bash/brec/record_structs.py | 5 + Mopy/bash/game/morrowind/records.py | 199 ++++++++++++++++++++++++---- 3 files changed, 189 insertions(+), 25 deletions(-) diff --git a/Mopy/bash/brec/basic_elements.py b/Mopy/bash/brec/basic_elements.py index b9bdaae1d1..b03db99e39 100644 --- a/Mopy/bash/brec/basic_elements.py +++ b/Mopy/bash/brec/basic_elements.py @@ -554,6 +554,16 @@ class MelStruct(MelBase): def __init__(self, subType, struct_format, *elements): """:type subType: bytes :type struct_format: unicode""" + # Sometimes subrecords have to preserve non-aligned sizes, check that + # we don't accidentally pad those to alignment + if (not struct_format.startswith(u'=') and + struct.calcsize(struct_format) != struct.calcsize( + u'=' + struct_format)): + raise SyntaxError( + u"Automatic padding inserted for struct format '%s', this is " + u"almost certainly not what you want. Prepend '=' to preserve " + u"the unaligned size or manually pad with 'x' to avoid this " + u"error." % struct_format) self.subType, self.struct_format = subType, struct_format self.attrs,self.defaults,self.actions,self.formAttrs = MelBase.parseElements(*elements) # Check for duplicate attrs - can't rely on MelSet.getSlotsUsed only, diff --git a/Mopy/bash/brec/record_structs.py b/Mopy/bash/brec/record_structs.py index 27ba53236d..656939c905 100644 --- a/Mopy/bash/brec/record_structs.py +++ b/Mopy/bash/brec/record_structs.py @@ -46,6 +46,11 @@ def __init__(self,*elements): element.getDefaulters(self.defaulters,'') element.getLoaders(self.loaders) element.hasFids(self.formElements) + for sig_candidate in self.loaders: + if len(sig_candidate) != 4 or not isinstance(sig_candidate, bytes): + raise SyntaxError(u"Invalid signature '%s': Signatures must " + u'be bytestrings and 4 bytes in ' + u'length.' % sig_candidate) def getSlotsUsed(self): """This function returns all of the attributes used in record instances diff --git a/Mopy/bash/game/morrowind/records.py b/Mopy/bash/game/morrowind/records.py index 09632ada90..382e783778 100644 --- a/Mopy/bash/game/morrowind/records.py +++ b/Mopy/bash/game/morrowind/records.py @@ -23,6 +23,8 @@ """This module contains the Morrowind record classes. Also contains records and subrecords used for the saves - see MorrowindSaveHeader for more information.""" +from collections import OrderedDict + from ... import brec from ...bolt import Flags from ...brec import MelBase, MelSet, MelString, MelStruct, MelArray, \ @@ -31,7 +33,7 @@ MelOptStruct, MelCounter, MelRefScale, MelOptSInt32, MelRef3D, \ MelOptFloat, MelOptUInt32, MelIcons, MelFloat, null1, null3, MelSInt32, \ MelFixedString, FixedString, AutoFixedString, MreGmstBase, MelOptUInt8, \ - MreLeveledListBase, MelUInt16 + MreLeveledListBase, MelUInt16, null4, SizeDecider, MelLists, null2 if brec.MelModel is None: class _MelModel(MelGroup): @@ -44,6 +46,35 @@ def __init__(self): #------------------------------------------------------------------------------ # Utilities ------------------------------------------------------------------- +#------------------------------------------------------------------------------ +class MelAIData(MelStruct): + """Handles the AIDT subrecord shared between CREA and NPC_.""" + _ai_flags = Flags(0, Flags.getNames( + u'ai_weapon', + u'ai_armor', + u'ai_clothing', + u'ai_books', + u'ai_ingredient', + u'ai_picks', + u'ai_probes', + u'ai_lights', + u'ai_apparatus', + u'ai_repair_items', + u'ai_misc', + u'ai_spells', + u'ai_magic_items', + u'ai_potions', + u'ai_training', + u'ai_spellmaking', + u'ai_enchanting', + u'ai_repair', + )) + + def __init__(self): + super(MelAIData, self).__init__(b'AIDT', u'Bs3B3sI', u'ai_hello', + (u'aidt_unknown1', null1), u'ai_fight', u'ai_flee', u'ai_alarm', + (u'aidt_unknown2', null3), (self._ai_flags, u'ai_flags')), + #------------------------------------------------------------------------------ class MelAIAccompanyPackage(MelOptStruct): """Deduplicated from AI_E and AI_F (see below).""" @@ -183,7 +214,7 @@ class MelSavesOnly(MelSequential): def __init__(self, *elements): super(MelSavesOnly, self).__init__(*(MelUnion({ True: element, - False: MelNull(b'ANY') + False: MelNull(next(iter(element.signatures))), }, decider=SaveDecider()) for element in elements)) #------------------------------------------------------------------------------ @@ -500,26 +531,6 @@ class MreCrea(MelRecord): u'skeleton_blood', u'metal_blood', )) - _ai_flags = Flags(0, Flags.getNames( - u'ai_weapon', - u'ai_armor', - u'ai_clothing', - u'ai_books', - u'ai_ingredient', - u'ai_picks', - u'ai_probes', - u'ai_lights', - u'ai_apparatus', - u'ai_repair_items', - u'ai_misc', - u'ai_spells', - u'ai_magic_items', - u'ai_potions', - u'ai_training', - u'ai_spellmaking', - u'ai_enchanting', - u'ai_repair', - )) melSet = MelSet( MelMWId(), @@ -540,9 +551,7 @@ class MreCrea(MelRecord): MelRefScale(), MelItems(), MelMWSpells(), - MelStruct(b'AIDT', u'Bs3B3sI', u'ai_hello', (u'unknown1', null1), - u'ai_fight', u'ai_flee', u'ai_alarm', (u'unknown2', null3), - (_ai_flags, u'ai_flags')), + MelAIData(), MelDestinations(), MelAIPackages(), ) @@ -804,3 +813,143 @@ class MreLock(MelRecord): MelMWIcon(), ) __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreLtex(MelRecord): + """Landscape Texture.""" + rec_sig = b'LTEX' + + melSet = MelSet( + MelMWId(), + MelUInt32(b'INTV', u'landscape_index'), + MelString(b'DATA', u'landscape_texture_name'), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreMgef(MelRecord): + """Magic Effect.""" + rec_sig = b'MGEF' + + _mgef_flags = Flags(0, Flags.getNames( + (9, u'spellmaking'), + (10, u'enchanting'), + (11, u'negative'), + )) + + ##: No MelMWId, will that be a problem? + ##: This will be a pain. They're hardcoded like in Oblivion, but use an int + # index instead of a fourcc (i.e. the EDID in Oblivion). + # Bad names to match other games (MGEF is scary since it's littered + # implicity all over our codebase still) + melSet = MelSet( + MelUInt32(b'INDX', u'mgef_index'), + MelStruct(b'MEDT', u'If4I3f', u'school', u'base_cost', + (_mgef_flags, u'flags'), u'mgef_red', u'mgef_green', u'mgef_blue', + u'speed_x', u'size_x', u'size_cap'), + MelMWIcon(), + MelString(b'PTEX', u'particle_texture'), + MelString(b'BSND', u'boltSound'), + MelString(b'CSND', u'castingSound'), + MelString(b'HSND', u'hitSound'), + MelString(b'ASND', u'areaSound'), + MelString(b'CVFX', u'casting_visual'), + MelString(b'BVFX', u'bolt_visual'), + MelString(b'HVFX', u'hit_visual'), + MelString(b'AVFX', u'are_visual'), + MelDescription(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreMisc(MelRecord): + """Misc. Item.""" + rec_sig = b'MISC' + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelStruct(b'MCDT', u'fI4s', u'misc_weight', u'misc_value', + (u'unknown1', null4)), + MelScriptId(), + MelMWIcon(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreNpc(MelRecord): + """Non-Player Character.""" + rec_sig = b'NPC_' + + _npc_flags = Flags(0, Flags.getNames( + (0, u'female'), + (1, u'essential'), + (2, u'respawn'), + (3, u'default_unknown'), # always set + (4, u'autoCalc'), # Bad name to match other games + (10, u'skeleton_blood'), + (11, u'metal_blood'), + )) + + class MelNpcData(MelLists): + """Converts attributes and skills into lists.""" + _attr_indexes = OrderedDict([ + (u'npc_level', 0), (u'attributes', slice(1, 9)), + (u'skills', slice(9, 36)), (u'unknown2', 36), (u'npc_health', 38), + (u'npc_spell_points', 39), (u'npc_fatigue', 40), + (u'npc_disposition', 41), (u'npc_reputation', 42), + (u'npc_rank', 43), (u'unknown3', 44), (u'npc_gold', 45), + ]) + + class NpcDataDecider(SizeDecider): + """At load time we can decide based on the subrecord size, but at dump + time we need to consider the auto-calculate flag instead.""" + can_decide_at_dump = True + + def decide_dump(self, record): + return 12 if record.npc_flags.autoCalc else 52 + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelString(b'RNAM', u'race_name'), + MelString(b'CNAM', u'class_name'), + MelString(b'ANAM', u'faction_name'), + MelString(b'BNAM', u'head_model'), + MelString(b'KNAM', u'hair_model'), + MelScriptId(), + MelUnion({ + 12: MelStruct(b'NPDT', u'H3B3sB', u'npc_level', u'npc_disposition', + u'npc_reputation', u'npc_rank', (u'unknown1', null3), + u'npc_gold'), + 52: MelNpcData(b'NPDT', u'H35Bs3H3BsI', u'npc_level', + (u'attributes', [0] * 8), (u'skills', [0] * 27), + (u'unknown2', null1), u'npc_health', u'npc_spell_points', + u'npc_fatigue', u'npc_disposition', u'npc_reputation', + u'npc_rank', (u'unknown3', null1), u'npc_gold'), + }, decider=NpcDataDecider()), + MelUInt32(b'FLAG', (_npc_flags, u'npc_flags')), + MelItems(), + MelMWSpells(), + MelAIData(), + MelDestinations(), + MelAIPackages(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MrePgrd(MelRecord): + """Path Grid.""" + rec_sig = b'PGRD' + + melSet = MelSet( + MelStruct(b'DATA', u'2I2sH', u'pgrd_x', u'pgrd_y', + (u'unknown1', null2), u'point_count'), + MelMWId(), + # Could be loaded via MelArray, but are very big and not very useful + MelBase(b'PGRP', u'point_array'), + MelBase(b'PGRC', u'point_edges'), + ) + __slots__ = melSet.getSlotsUsed() From 46cabb59c3523064424b81a58efde2e52c7cc4f9 Mon Sep 17 00:00:00 2001 From: Infernio Date: Sat, 17 Oct 2020 23:42:41 +0200 Subject: [PATCH 7/9] TES3: Define PROB, RACE, REGN, REPA and SCPT TES3: Define PROB TES3: Define RACE TES3: Define REGN TES3: Define REPA TES3: Define SCPT --- Mopy/bash/game/morrowind/records.py | 101 +++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/Mopy/bash/game/morrowind/records.py b/Mopy/bash/game/morrowind/records.py index 382e783778..3d5cd3714a 100644 --- a/Mopy/bash/game/morrowind/records.py +++ b/Mopy/bash/game/morrowind/records.py @@ -33,7 +33,8 @@ MelOptStruct, MelCounter, MelRefScale, MelOptSInt32, MelRef3D, \ MelOptFloat, MelOptUInt32, MelIcons, MelFloat, null1, null3, MelSInt32, \ MelFixedString, FixedString, AutoFixedString, MreGmstBase, MelOptUInt8, \ - MreLeveledListBase, MelUInt16, null4, SizeDecider, MelLists, null2 + MreLeveledListBase, MelUInt16, null4, SizeDecider, MelLists, null2, \ + MelTruncatedStruct, MelColor, MelStrings if brec.MelModel is None: class _MelModel(MelGroup): @@ -422,6 +423,9 @@ class MreCell(MelRecord): (u'new_exterior_cell_y', None)), MelReference(), ), + ##: Move this into a dedicated Mob* class instead - difficult to + # manipulate otherwise, tons of duplicate signatures and a distributor + # is impossible due to the lack of static separators in the record. MelGroups(u'persistent_children', MelReference(), ), @@ -953,3 +957,98 @@ class MrePgrd(MelRecord): MelBase(b'PGRC', u'point_edges'), ) __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreProb(MelRecord): + """Probe Item.""" + rec_sig = b'PROB' + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelStruct(b'PBDT', u'fIfI', u'probe_weight', u'probe_value', + u'probe_quality', u'probe_uses'), + MelMWIcon(), + MelScriptId(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreRace(MelRecord): + """Race.""" + rec_sig = b'RACE' + + _race_flags = Flags(0, Flags.getNames(u'playable', u'beast_race')) + + melSet = MelSet( + MelMWId(), + MelMWFull(), + # Bad names to match other games (race patcher) + MelStruct(b'RADT', u'14i16I4fI', u'skill1', u'skill1Boost', u'skill2', + u'skill2Boost', u'skill3', u'skill3Boost', u'skill4', + u'skill4Boost', u'skill5', u'skill5Boost', u'skill6', + u'skill6Boost', u'skill7', u'skill7Boost', u'maleStrength', + u'femaleStrength', u'maleIntelligence', u'femaleIntelligence', + u'maleWillpower', u'femaleWillpower', u'maleAgility', + u'femaleAgility', u'maleSpeed', u'femaleSpeed', u'maleEndurance', + u'femaleEndurance', u'malePersonality', u'femalePersonality', + u'maleLuck', u'femaleLuck', u'maleHeight', u'femaleHeight', + u'maleWeight', u'femaleWeight', (_race_flags, u'race_flags')), + MelMWSpells(), + MelDescription(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreRegn(MelRecord): + """Region.""" + rec_sig = b'REGN' + + melSet = MelSet( + MelMWId(), + MelMWFull(), + MelTruncatedStruct(b'WEAT', u'=10B', u'chance_clear', u'chance_cloudy', + u'chance_foggy', u'chance_overcast', u'chance_rain', + u'chance_thunder', u'chance_ash', u'chance_blight', + u'chance_snow', u'chance_blizzard', old_versions={u'8B'}), + MelString(b'BNAM', u'sleep_creature'), + MelColor(), + MelGroups(u'sound_chances', + MelStruct(b'SNAM', u'=32sB', (FixedString(32), u'sound_name'), + u'sound_chance'), + ), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreRepa(MelRecord): + """Repair Item.""" + rec_sig = b'REPA' + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelStruct(b'RIDT', u'f2If', u'repa_weight', u'repa_value', + u'repa_uses', u'repa_quality'), + MelMWIcon(), + MelScriptId(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreScpt(MelRecord): + """Script.""" + rec_sig = b'SCPT' + + melSet = MelSet( + # Yes, the usual NAME sits in this subrecord instead + MelStruct(b'SCHD', u'32s5I', (FixedString(32), u'mw_id'), + u'num_shorts', u'num_longs', u'num_floats', u'script_data_size', + u'local_var_size'), + MelStrings(b'SCVR', u'script_variables'), + MelBase(b'SCDT', u'compiled_script'), + MelString(b'SCTX', u'script_source'), + ) + __slots__ = melSet.getSlotsUsed() From 47e9773287cd7e2636c57d0a18802ce2b51c5de0 Mon Sep 17 00:00:00 2001 From: Infernio Date: Sun, 18 Oct 2020 18:06:05 +0200 Subject: [PATCH 8/9] TES3: Define SKIL, SNDG, SOUN, SPEL and SSCR TES3: Define SKIL Another one without a NAME subrecord. On the bright side, I have some ideas for how we could handle this one and MGEF. Proposal 1: class MreSkil(MelRecord): # ... melSet = MelSet( # ... ).with_virtual_elements({ u'mw_id': lambda record: skill_names[record.skill_index], }) Those virtual elements would be created after the regular loadData calls are done. Pros: - Nicely solves the issue here: the SKIL and MGEF records have hardcoded meanings based on their INDX subrecords - The part of the code that lands in records code is almost entirely declarative. Not perfect, but the only way to move more of it into brec is to move the whole definition into brec, which feels inflexible and too specific Cons: - Changing the virtual element's value does not update the original element's value. This is not a problem here since the INDX values are hardcoded anyways and we only need to have the NAME for reading, but limits this solution's future usefulness - This needs to be implemented in MelSet, which is nasty and feels like the wrong place to put this - see the next proposal Proposal 2: class MreSkil(MelRecord): # ... melSet = MelSet( # ... ) virtual_elements = { u'mw_id': lambda record: skill_names[record.skill_index], } Pros: - The same pros as the first proposal, but also doesn't need to land in MelSet Cons: - Feels... disconnected from the actual record definition. When I look at this I don't make the connection that these are going to be added to the final set of record elements - As a result of the above - maybe MelSet really is the right place for this? MelSet kind of *is* the definition of the record, so elements that get virtually added would make sense to be there... There may also be entirely different solutions to this that don't involve virtually adding an element to the record that I haven't thought of yet. TES3: Define SNDG TES3: Define SOUN TES3: Define SPEL TES3: Define SSCR --- Mopy/bash/game/morrowind/records.py | 74 +++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/Mopy/bash/game/morrowind/records.py b/Mopy/bash/game/morrowind/records.py index 3d5cd3714a..b7d69045dc 100644 --- a/Mopy/bash/game/morrowind/records.py +++ b/Mopy/bash/game/morrowind/records.py @@ -1052,3 +1052,77 @@ class MreScpt(MelRecord): MelString(b'SCTX', u'script_source'), ) __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreSkil(MelRecord): + """Skill.""" + rec_sig = b'SKIL' + + ##: No MelMWId, will that be a problem? + melSet = MelSet( + MelUInt32(b'INDX', u'skill_index'), + MelStruct(b'SKDT', u'2I4f', u'skill_attribute', + u'skill_specialization', u'use_value_1', u'use_value_2', + u'use_value_3', u'use_value_4'), + MelDescription(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreSndg(MelRecord): + """Sound Generator.""" + rec_sig = b'SNDG' + + melSet = MelSet( + MelMWId(), + MelUInt32(b'DATA', u'sdng_type'), + MelString(b'CNAM', u'creature_name'), + ##: Investigate what this is and if we should use it instead of NAME + MelString(b'SNAM', u'sound_id'), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreSoun(MelRecord): + """Sound.""" + rec_sig = b'SOUN' + + melSet = MelSet( + MelMWId(), + MelString(b'FNAM', u'sound_filename'), + MelStruct(b'DATA', u'=3B', u'atten_volume', u'min_range', + u'max_range'), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreSpel(MelRecord): + """Spell.""" + rec_sig = b'SPEL' + + _spell_flags = Flags(0, Flags.getNames( + u'auto_calc', + u'pc_start', + u'always_suceeds', + )) + + melSet = MelSet( + MelMWId(), + MelMWFull(), + # Bad names to match other games (tweaks) + MelStruct(b'SPDT', u'3I', u'spellType', u'cost', + (_spell_flags, u'spell_flags')), + MelEffects(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreSscr(MelRecord): + """Start Script.""" + rec_sig = b'SSCR' + + melSet = MelSet( + MelString(b'DATA', u'unknown_digits'), # series of ASCII digits + MelMWId(), + ) + __slots__ = melSet.getSlotsUsed() From 03f53d01acc204beca23a74bace7d6f6c3d8310e Mon Sep 17 00:00:00 2001 From: Infernio Date: Sun, 18 Oct 2020 18:37:45 +0200 Subject: [PATCH 9/9] TES3: Define STAT and WEAP TES3: Define STAT TES3: Define WEAP That's the last of the records defined. Doesn't mean we're anywhere close to loading them, of course. TES3: Import all record classes Still can't load them since the Mob* classes are completely wrong for Morrowind :P Note I dropped the pack_format stuff from there - those don't make any sense for Morrowind. We'll need 480-pt3 and a proper RecordHeader hierarchy per game to handle this. --- Mopy/bash/game/morrowind/__init__.py | 27 +++++++++++++-------- Mopy/bash/game/morrowind/records.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/Mopy/bash/game/morrowind/__init__.py b/Mopy/bash/game/morrowind/__init__.py index 320434c3c4..e91c1ae654 100644 --- a/Mopy/bash/game/morrowind/__init__.py +++ b/Mopy/bash/game/morrowind/__init__.py @@ -106,7 +106,13 @@ class Esp(GameInfo.Esp): @classmethod def init(cls): cls._dynamic_import_modules(__name__) - from .records import MreTes3 + from .records import MreActi, MreAlch, MreAppa, MreArmo, MreBody, \ + MreBook, MreBsgn, MreCell, MreClas, MreClot, MreCont, MreCrea, \ + MreDial, MreDoor, MreEnch, MreFact, MreGmst, MreGlob, MreInfo, \ + MreIngr, MreLand, MreLevc, MreLevi, MreLigh, MreLock, MreLtex, \ + MreMgef, MreMisc, MreNpc, MrePgrd, MreProb, MreRace, MreRegn, \ + MreRepa, MreScpt, MreSkil, MreSndg, MreSoun, MreSpel, MreSscr, \ + MreStat, MreTes3, MreWeap # Setting RecordHeader class variables - Morrowind is special header_type = brec.RecordHeader header_type.rec_header_size = 16 @@ -125,18 +131,19 @@ def init(cls): b'LIGH', b'ENCH', b'NPC_', b'ARMO', b'CLOT', b'REPA', b'ACTI', b'APPA', b'LOCK', b'PROB', b'INGR', b'BOOK', b'ALCH', b'LEVI', b'LEVC', b'CELL', b'LAND', b'PGRD', b'SNDG', b'DIAL', b'INFO'] - # +SSCR? in xEdit: to be confirmed - # TODO(inf) Everything up to this TODO correct, the rest may not be yet - header_type.pack_formats = {0: u'=4sI4s2I'} - header_type.pack_formats.update( - {x: u'=4s4I' for x in {1, 6, 7, 8, 9, 10}}) - header_type.pack_formats.update({x: u'=4sIi2I' for x in {2, 3}}) - header_type.pack_formats.update({x: u'=4sIhh2I' for x in {4, 5}}) header_type.valid_header_sigs = set( header_type.top_grup_sigs + [b'TES3']) - brec.MreRecord.type_class = {x.rec_sig: x for x in (MreTes3,)} + brec.MreRecord.type_class = {x.rec_sig: x for x in ( + MreActi, MreAlch, MreAppa, MreArmo, MreBody, MreBook, MreBsgn, + MreCell, MreClas, MreClot, MreCont, MreCrea, MreDial, MreDoor, + MreEnch, MreFact, MreGmst, MreGlob, MreInfo, MreIngr, MreLand, + MreLevc, MreLevi, MreLigh, MreLock, MreLtex, MreMgef, MreMisc, + MreNpc, MrePgrd, MreProb, MreRace, MreRegn, MreRepa, MreScpt, + MreSkil, MreSndg, MreSoun, MreSpel, MreSscr, MreStat, MreTes3, + MreWeap, + )} brec.MreRecord.simpleTypes = ( - set(brec.MreRecord.type_class) - {b'TES3'}) + set(brec.MreRecord.type_class) - {b'TES3', b'CELL', b'DIAL'}) cls._validate_records() GAME_TYPE = MorrowindGameInfo diff --git a/Mopy/bash/game/morrowind/records.py b/Mopy/bash/game/morrowind/records.py index b7d69045dc..9a62000acc 100644 --- a/Mopy/bash/game/morrowind/records.py +++ b/Mopy/bash/game/morrowind/records.py @@ -1126,3 +1126,39 @@ class MreSscr(MelRecord): MelMWId(), ) __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreStat(MelRecord): + """Static.""" + rec_sig = b'STAT' + + melSet = MelSet( + MelMWId(), + MelModel(), + ) + __slots__ = melSet.getSlotsUsed() + +#------------------------------------------------------------------------------ +class MreWeap(MelRecord): + """Weapon.""" + rec_sig = b'WEAP' + + _weapon_flags = Flags(0, Flags.getNames( + u'ignore_normal_weapon_resistance', + u'is_silver', + )) + + melSet = MelSet( + MelMWId(), + MelModel(), + MelMWFull(), + MelStruct(b'WPDT', u'fI2H2fH6BI', u'weapon_weight', u'weapon_value', + u'weapon_type', u'weapon_health', u'weapon_speed', u'weapon_reach', + u'enchant_points', u'chop_minimum', u'chop_maximum', + u'slash_minimum', u'slash_maximum', u'thrust_minimum', + u'thrust_maximum', (_weapon_flags, u'weapon_flags')), + MelMWIcon(), + MelMWEnchantment(), + MelScriptId(), + ) + __slots__ = melSet.getSlotsUsed()