Repository navigation
Conversation
A gendered field whose male and female halves are separate subrecords can have its arm entered once per half when another subrecord sits between them. Each entry built a new pair and replaced the field, so the half read first was lost, in the direct parse and in the overlay, and a write then dropped that half's subrecords. The parse and overlay helpers that a split pair reaches now take the field's current value as existing and replace only the halves they read. A marker followed by nothing that parses keeps the half it had. In the converter forms a held half is replaced only by a subrecord converted to that half; a subrecord that neither converter maps (Starfield's FLLD and XFLG on ARMA models) goes to a half the pair does not hold yet, from an earlier entry or from this one, and the overlay keeps it only if the item reads something. The lazy GenderedItemBinaryOverlay keeps, per half, the memory that half starts at, taken from this entry or copied from the previous one. FactorySkipMarkersPreRead read the next subrecord's header and skipped its content before checking whether it was a marker, so it swallowed the subrecord that ended a separated pair. It now peeks at the header and stops without moving. In generation, IsSplitPair (a trigger and no single record type) decides where existing is passed, in the parse and in the overlay. A split pair that would reach a helper which cannot keep a half across entries is refused at generation: the ParseRequired overloads without markers, which always read the first item as male; the contentMarker and MarkerWithinItem parses; and in the overlay, the marker-ahead form that takes translationParams and the branch that reads both halves from one subrecord. No field reaches any of them. The generated CopyInFromBinary sets each split pair to null before the parse and puts the old value back only if the arm was never entered, so a pair present in the bytes replaces the target's pair whole, like every other present field. When a split pair shares some subrecord types with other fields, the generator routed all of its types by the last parsed field. Starfield ARMA's WorldModel shares FLLD and XFLG with FirstPersonModel and ExtraLightLayers, so a MOD3 after MOD4 fell into the ExtraLightLayers branch. A split pair's own subrecord types now go straight to its arm; only the shared ones are routed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Output of Mutagen.Bethesda.Generator.All: the 18 files that hold a split gendered pair (ARMA, ARMO, ASTP, RACE and Rank in Skyrim and Fallout 4; ARMA, ARMO, RACE and Rank in Starfield; RACE and Rank in Oblivion and Fallout 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds records from raw bytes and reads each through the direct parser and the overlay, in Skyrim, Starfield, Fallout 4, Oblivion and Fallout 3: ARMA models in the order the CBBEtoUBE v1.5 converter writes them (MOD2, MOD4, MOD3), the Creation Kit order as a control, interleaved texture and ASTP title pairs, an ARMO whose MOD2 and MOD4 are separated, RACE HeadData, SkeletalModel and a repeated BodyData block, a marker with nothing after it, unconverted FLLD subrecords in the WorldModel arm, a male-only Starfield world model, a write of the interleaved ARMA, and CopyInFromBinary into a filled ARMA. The ARMA records carry a zeroed DNAM, as every real ARMA does, because the overlay reads it when writing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A gendered field whose male and female halves are separate subrecords loses the half it read first when another subrecord sits between the two. Each generated case arm reads the pair, builds a new
GenderedItemand assigns it, so when the arm is entered again for the later half it replaces the pair. The direct parse and the overlay both do this, and a write then omits the lost subrecords.The order that triggers it
An ARMA written as
rather than the Creation Kit's
MOD2, MO2T, MOD3, MO3T, MOD4, MO4T, MOD5. xEdit and the game both read it. Mutagen reads both male models as absent, and a write carries onlyMOD3andMOD5.Which fields are affected
WorldModel,FirstPersonModelMOD2/MOD3,MOD4/MOD5SkinTexture,TextureSwapListNAM0/NAM1,NAM2/NAM3WorldModelMOD2/MOD4ParentTitle,TitleMPRT/FPRT,MCHT/FCHTSkeletalModel,HeadDataMNAM/FNAM;NAM0ahead of eachBodyData,BehaviorGraphNAM1/NAM3block, thenMNAM/FNAMTitleMNAM/FNAMSkeletalModelMNAM/FNAMRace.csand routed with other RACE fieldsVoices,DecapitateArmors,DefaultHairColorsRows are Skyrim unless named; the other games get the same change on their split pairs.
The fix
When a gendered arm is entered and the field already holds a pair, the newly read half is merged into it instead of replacing it.
GenderedItemBinaryTranslationandGenderedItemBinaryOverlay: the helpers a split pair reaches take an optionalexistingpair and replace only the halves they read. A held half is never replaced by an unconverted subrecord such as Starfield'sFLLD.GenderedTypeBinaryTranslationGeneration:IsSplitPairdecides whereexistingis passed. A split pair routed to a helper that cannot merge, such asParseRequiredwithout markers, is refused at generation.PluginTranslationModule:CopyInFromBinarystarts each split pair empty, so a pair present in the bytes is replaced whole. A split pair's own subrecord types go straight to its arm, which fixes Starfield ARMA, where aMOD3afterMOD4fell into theExtraLightLayersbranch.I chose merging over scanning ahead for the other marker, which would still leave the arm entered again at the later marker.
Open questions
CopyInFromBinaryblanks each split pair before the parse and restores it if the arm never ran. I can move this onto the parse loop's per-record state (PreviousParse) instead.Behaviour change
None for callers: merging happens only within one record. On malformed input the overlay now returns what the direct parse already did.
How tested
GenderedSplitPairTestsin Skyrim, Starfield, Fallout 4, Oblivion and Fallout 3: 18 cases on raw record bytes, each read through the direct parser and the overlay. 16 fail ondev, in every game; the two that pass are the Creation Kit order control and a check that copy-in still replaces a pair present in the bytes whole.Mutagen.Records.slnbuilds in Release with 0 errors, and the unit tests pass onnet9.0andnet10.0. Every ARMA, ARMO, RACE, ASTP and FACT in Skyrim.esm and its DLC (6,586 records), read by both paths and written back, hashes the same as ondev. I had no data for Fallout 4, Starfield, Oblivion or Fallout 3, so please run their passthrough tests before merging.A separate issue, not fixed here:
ArmorAddonBinaryOverlay.GetWeightSliderEnabledCustom()throws on an ARMA with noDNAM(#699).I found this through houseCARL, a tool built on Mutagen, which read and then rewrote an ARMA from a plugin generated by CBBEtoUBE and lost its male models.
🤖 Generated with Claude Code