Skip to content
dream_archive

Library Morrowind active

dream_archive

Read, extract and write Bethesda BSA and BA2 archives, from Morrowind to Starfield, as a Jess package in Luau.

"org.dreamweave.dream-archive" = "2.0.0"
Stable
2.0.0 2026-10-21
License
MIT OR Apache-2.0

Overview

Every Bethesda game since Morrowind keeps its meshes, textures and sounds in archives: BSA from Morrowind to Skyrim, BA2 from Fallout 4 on. Each generation changed the layout, the hashes and the compression, and a tool that reads one of them usually reads none of the others.

dreamarchive reads all three generations through one type. `Archive::openpath` reads the header, and the same calls then list, look up, read, stream and extract a Morrowind BSA, a Skyrim SE BSA or a Fallout 4 BA2. Where a format really is different, its own module has the rest: hash-only TES4 archives, BA2 texture metadata, each builder's settings. It writes every family it reads, from bytes, files, directories, or entries of another archive.

It is Luau all the way down, a Jess package with no native code. Archive paths stay bytes: the package never guesses a code page, and it tells you when a path cannot be stored or extracted safely instead of writing it somewhere else.

Three generations, one reader

The header says which family an archive is. After that, listing, lookup, reading and extraction are the same calls for all of them.

  1. source

    Any supported archive

    Morrowind's BSA, the TES4 BSA of Oblivion through Skyrim SE, and the BA2 of Fallout 4 through Starfield.

  2. tool

    Archive

    Detects the family, indexes every entry by path and hash, and reads straight from the file.

  3. result

    Bytes, readers, files

    A string of bytes, or a directory tree written one file at a time, each file atomically.

How the pieces relate
  • Any supported archive Archive
  • Archive Bytes, readers, files
  • Builders write Any supported archive
  • Any Jess package @dream-archive Archive
local dreamArchive = require '@dream-archive'

local archive = dreamArchive.openPath 'Morrowind.bsa'
print(archive:format())

for _, entry in archive:entries() do
  if entry.path == [[meshes\m\probe_journeyman_01.nif]] then
    print(entry.index, entry.size)
  end
end

local texture = archive:readFileRequired [[Textures\Tx_Wood.DDS]]
print(#texture, 'bytes')

What it covers§

FamilyGamesRead and extractWrite
TES3 BSAMorrowindyesyes
TES4 BSA, versions 103 to 105Oblivion, Fallout 3, New Vegas, Skyrim, Skyrim SEyes, including hash-only archivesyes
BA2 GNRLFallout 4, Fallout 76, Starfieldyesyes
BA2 DX10Fallout 4, Fallout 76, Starfieldyes, as DDS filesfrom DDS files or texture metadata
BA2 GNMFconsole texturesmetadata onlyno

Console layouts and XMem compression are refused with an error.

Documentation§

The package's reference is src/init.jsl and the modules beside it: every function is documented where it is written, and jess luau-types prints the module's types. The guide that explained the Rust crate lives with it on the rust branch; the Luau package's own guide is being written.

Want a command line instead of a library? dream_archivetool lists, verifies, diffs, extracts, creates and updates archives, built on this package.

  1. 2.0.0 stable 2026-10-21

    dream_archive is a Jess package, written in Luau; the Rust crate continues on the rust branch at 1.1.0.

    Breaking changes

    • The library is the Jess package org.dreamweave.dream-archive, module @dream-archive, installed with jess rather than Cargo. The Rust crate dream_archive stays at 1.1.0 on crates.io and on this repository's rust branch, and is no longer developed on main.
    org.dreamweave.dream-archive-2.0.0.jpk 255.3 kB · jpk
    Verify · sha256 281c27bf86a2…623035730
    Release
    2.0.0 (stable)
    Size
    261432 bytes
    SHA-256
    281c27bf86a2b9ea14dc9baa4959a9a9d213784332acfefcc9a9a9e623035730
    Commit
    7af9ffda3828
    Package
    org.dreamweave.dream-archive
    Content digest
    2542758bf2af0319bcecca13eb44158c43b87733d207eb02f13d647412ee9dc1
    Sources
    1

    Check it yourself: sha256sum org.dreamweave.dream-archive-2.0.0.jpk must print the digest above.

  2. 1.1.0 stable 2026-09-30

    Relicensed as MIT OR Apache-2.0 and built on l3i 1.1.0.

    Changed

    • Relicensed from GPL-3.0 to MIT OR Apache-2.0: this is the first release under the new terms, and releases up to and including 1.0.0 stay GPL.
    • Requires dream-path 1.1, its first MIT OR Apache-2.0 release.
    • l3i 1.1.0 comes from crates.io; the @dream/archive module is unchanged.
    dream_archive-1.1.0.crate 7.1 MB · crate
    Verify · sha256 ef05ce9b63b8…01dbc24c7
    Release
    1.1.0 (stable)
    Size
    7404128 bytes
    SHA-256
    ef05ce9b63b87ff34e29708aa5d58829608063a64cf236420adbba101dbc24c7
    Commit
    6cf4005005b5
    Sources
    1

    Check it yourself: sha256sum dream_archive-1.1.0.crate must print the digest above.

  3. 1.0.0 stable 2026-09-29

    First stable release: the Luau binding is an l3i extension providing @dream/archive, every archive family looks entries up by hash, and lookups and zlib reads allocate less.

    Breaking changes

    • The luau feature (lua is the old name of the same feature) binds the crate as an l3i extension, luau::ArchiveExtension, providing @dream/archive. dream_archive::lua, the mlua types and the standalone-lua feature are gone.
    • Building with the luau feature needs l3i's toolchain: clang, lld and cross-language thin LTO.
    • Every open* in Luau returns the one Archive type; archive:entries() is a view of entry handles, not a table of tables.
    • Luau entry hashes are integers: entry.hash and entry.folderHash replace the old tables and fields, with hashHex and folderHashHex for the hex spelling. BA2 hashes are directoryHash, fileHash and extensionHash; sizes and offsets are size, storedSize and offset on every format.
    • Luau integer arguments must be exact: readEntry(1.5) is an error instead of a rounded read.
    • bsa::Error::InvalidFileRecordFlags is gone. Nothing returned it: both flag bits of a TES4 file record are read as the format defines them.

    Added

    • get_by_hash, get_id_by_hash and contains_hash on every archive family, get_id by path, and HashFields::from_numeric.
    • archive:readInto(entry, buffer, offset) in Luau decodes a member straight into a buffer, and archive:getByHash and archive:containsHash take the family's hash shape.
    • Luau type definitions for the whole module, checked by Luau's own type checker in the tests behind the luau-analysis feature.
    • toBuffer() on every Luau builder; only ba2.Builder had it.
    • dream_archive::flate2, the re-exported flate2 whose Compression set_zlib_level takes, so naming a zlib level needs no flate2 dependency of your own.

    Changed

    • Path lookups normalize into a stack buffer instead of allocating: contains over 4096 distinct paths takes 0.39 ms instead of 1.03 ms (TES3) and 1.43 ms (TES4).
    • zlib members inflate from the archive through one inflater per thread: a 64 KiB TES4 member reads in 18 µs instead of 68 µs.
    • Walking entries from Luau is 5 to 23 times faster than with mlua, and small calls such as contains and readEntry take about half as long: 4096 TES3 entries take 0.73 ms instead of 6.15 ms.
    • dream-path 1.0 and l3i 1.0.0 come from crates.io; Rust 1.92.

    Fixed

    • Dx10Builder::add_dds_bytes and add_dds_file take DDS files whose depth field is 1, as many tools write for ordinary textures. A DDS file is a volume texture only when its flags have DDSD_DEPTH or its caps have DDSCAPS2_VOLUME.
    • A cubemap's extended DX10 DDS header has an array size of 1, one cube, as the DDS format and DirectXTex count it: extraction wrote 6, which DirectXTex-based tools read as six cubes, and add_dds_bytes and add_dds_file refused cubemaps saved by texconv. Cubemaps extracted by earlier versions, which say 6 over one cube's data, are still accepted.
    • Every family matches a lookup path the same way, with dream-path's normalization. BA2 lookups kept repeated separators and dropped a trailing one, so meshes//door.nif missed and meshes/door.nif/ found a member a BSA answered the other way round; hash-only TES4 lookups missed a//b/c.nif. Archives without names now hash the normalized path.
    • Luau entry.name is the part of the path after its last separator in every family. For a BA2 entry it was the whole stored path, the same as entry.path.
    • Luau entry.folder is "" for an entry at the root of a TES3 archive or a BA2, as it was for TES4, instead of nil; nil means the archive stores no names.
    • The crate builds with only the ba2 feature: it now enables the lz4_flex feature its compressor needs, which only bsa-tes4 turned on before. Builds with one BSA family alone no longer carry dead code.

    Migration

    Compose dream_archive::luau::ArchiveExtension into the host's l3i RuntimePlan. A host that still wants the dreamArchive global adds RuntimePolicy::compat_global("@dream/archive", "dreamArchive"). That release's Embedding Luau page listed what scripts must change; the Luau port's documentation replaces it.

    dream_archive-1.0.0.crate 7.1 MB · crate
    Verify · sha256 fe94718bdde6…1743d2225
    Release
    1.0.0 (stable)
    Size
    7408572 bytes
    SHA-256
    fe94718bdde6117e633510a184a036c88bad99d0a745c0d18de21301743d2225
    Commit
    94f37c74ce28
    Sources
    1

    Check it yourself: sha256sum dream_archive-1.0.0.crate must print the digest above.

Credits