Skip to content

Binary Utility

BinaryStringUtility

Bethesda games store their strings on disk in a single byte format, with a null terminator. There are some convenience parsing functions inside BinaryStringUtility to convert these to C# strings.

ToZString

This function assumes the entire input span is bytes that need to be converted to a string. There should be no null termination at the end, or it will be included in the resulting string.

ProcessWholeToZString

This function assumes the entire input span is part of the string, and may have a null termination. It will trim off the null termination if it exists, and return a string. Good for typical use.

var subRecordBytes = ...;
var subRecordFrame = meta.SubRecordFrame(someBytes.AsSpan());
string str = BinaryStringUtility.ProcessWholeToZString(subRecordFrame.Content);

ParseUnknownLengthString

Sometimes you might not be able to trim your input span to a string's contents exactly. This call will help by locating the first null byte, and retrieving the string up until that point.

var bytesOfUnknownLength = ...;
string str = BinaryStringUtility.ParseUnknownLengthString(bytesOfUnknownLength.AsSpan());
var amountParsed = str.Length + 1; // +1 for the null termination that was trimmed

SubRecord Iteration and Location

There are some convenience methods for iterating and locating Subrecords in a set of raw bytes in UtilityTranslation. These are extension methods onto HeaderConstants and MajorRecordFrame, so they can be used directly from those objects.

EnumerateSubrecords

Print some metadata for all Subrecords in a MajorRecord

var meta = HeaderConstants.Skyrim;
byte[] majorRecordBytes = ...;
var majorFrame = meta.MajorRecordFrame(majorRecordBytes.AsSpan());

foreach (KeyValuePair<RecordType, int> loc in majorFrame.EnumerateSubrecords())
{
    // Scope to the start of the subrecord's data, relative to the MajorRecord content
    var subRecordSpan = majorFrame.Content.Slice(loc.Value);
    var subHeader = meta.SubRecord(subRecordSpan);
    System.Console.WriteLine($"Found subrecord {loc.Key} at position {loc.Value}, with length {subHeader.ContentLength}");
}

TryFindFirstSubrecord

This call returns the first location of a subrecord type. Good for locating a specific subrecord

if (majorFrame.TryFindFirstSubrecord("EDID", out SubRecordFrame subFrame))
{
    System.Console.WriteLine($"Found EDID with length {subFrame.Header.ContentLength}: ");
    System.Console.WriteLine($"   {BinaryStringUtility.ProcessWholeToZString(subFrame.Content)}");
}

FindFirstSubrecords

This call returns the first location of each subrecord type queried for. Good for finding a set of subrecords in one pass.

var finds = majorFrame.FindFirstSubrecords("EDID", "FULL");
// If EDID found
if (finds[0] != -1)
{
    // Scope to the start of the subrecord's data, relative to the MajorRecord content
    var subRecordSpan = majorFrame.Content.Slice(finds[0]);
    var subFrame = meta.SubRecord(subRecordSpan);
    System.Console.WriteLine($"Found EDID with length {subFrame.Header.ContentLength}: ");
    System.Console.WriteLine($"   {BinaryStringUtility.ProcessWholeToZString(subFrame.Content)}");
}

// If FULL found
if (finds[1] != -1)
{
    // Scope to the start of the subrecord's data, relative to the MajorRecord content
    var subRecordSpan = majorFrame.Content.Slice(finds[1]);
    var subFrame = meta.SubRecord(subRecordSpan);
    System.Console.WriteLine($"Found FULL with length {subFrame.Header.ContentLength}: ");
    System.Console.WriteLine($"   {BinaryStringUtility.ProcessWholeToZString(subFrame.Content)}");
}

CompileFirstSubrecordLocations

Similar to FindFirstSubrecords, except the results are put into a Dictionary<RecordType, int> for cleaner use. Comes at the cost of higher overhead to construct this dictionary. Another low cost alternative is just using EnumerateSubrecords with a switch statement.

var edid = new RecordType("EDID");
var full = new RecordType("FULL");
var finds = majorFrame.CompileFirstSubrecordLocations(edid, full);

// If EDID found
if (finds.TryGetValue(edid, out var loc))
{
    // Scope to the start of the subrecord's data, relative to the MajorRecord content
    var subRecordSpan = majorFrame.Content.Slice(loc);
    var subFrame = meta.SubRecord(subRecordSpan);
    System.Console.WriteLine($"Found EDID with length {subFrame.Header.ContentLength}: ");
    System.Console.WriteLine($"   {BinaryStringUtility.ProcessWholeToZString(subFrame.Content)}");
}

// If FULL found
if (finds.TryGetValue(full, out loc))
{
    // Scope to the start of the subrecord's data, relative to the MajorRecord content
    var subRecordSpan = majorFrame.Content.Slice(loc);
    var subFrame = meta.SubRecord(subRecordSpan);
    System.Console.WriteLine($"Found FULL with length {subFrame.Header.ContentLength}: ");
    System.Console.WriteLine($"   {BinaryStringUtility.ProcessWholeToZString(subFrame.Content)}");
}

RecordLocator

RecordLocator is a convenience class that processes a mod stream and returns all the locations of Groups and MajorRecords.

It takes a few optional arguments:

  • RecordInterest, to limit the search to specific RecordTypes
  • Additional Criteria lambda, to add custom filter logic
using var stream = new MutagenReadStream(pathToMod, HeaderConstants.Skyrim);
RecordLocator.FileLocations locs = RecordLocator.FileLocations.GetFileLocations(
    stream,
    // Only search for NPCs
    new RecordInterest("NPC_"),
    // That are compressed
    additionalCriteria: (stream, recordType, recLength) =>
    {
        var majorHeader = stream.Meta.GetMajorRecord(stream);
        return majorHeader.IsCompressed;
    });

/// Can enumerate Group locations
foreach (var groupLoc in locs.GrupLocations)
{
   System.Console.WriteLine($"Group located at: {groupLoc}");
}

/// Can enumerate MajorRecord locations
foreach (KeyValuePair<long, (FormID FormID, RecordType Record)> recLoc in locs.ListedRecords)
{
   System.Console.WriteLine($"MajorRecord located at: {recLoc.Key}, {recLoc.Value.FormID}");
}

/// Can query a specific FormID's location
if (locs.TryGetSection(someFormID, out RangeInt64 loc))
{
   stream.Position = loc.Min;
   var header = stream.Meta.ReadMajorRecord(stream);
   System.Console.WriteLine($"MajorRecord type: {header.RecordType}");
}

/// Can query what record exists at a location
if (locs.TryGetRecord(loc: 0x1234, out (FormID FormID, RecordType Record) rec))
{
   System.Console.WriteLine($"{rec.Record}: {rec.FormID}");
}

/// Can enumerate the parent Group locations for a FormID
foreach (var groupLoc in locs.GetContainingGroupLocations(someFormID))
{
   stream.Position = groupLoc;
   var groupHeader = stream.Meta.ReadGroupHeader(stream);
   System.Console.WriteLine($"{someFormID} has parent group at: {groupLoc}.  Type: {groupHeader.GroupType}");
}

This class just provides a common use case of locating records. If more fine tuned logic is needed, just fall back to using Header Struct parsing yourself with the extra logic you need.

Note that this class uses FormID, and does not make use of the abstraction concepts found in ModKey, FormKey, FormLink

Decompression

MajorRecords with their compression flag enabled come in a compressed format. The byte content needs to be unzipped before it can be read.