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.
-
source
Any supported archive
Morrowind's BSA, the TES4 BSA of Oblivion through Skyrim SE, and the BA2 of Fallout 4 through Starfield.
-
tool
Archive
Detects the family, indexes every entry by path and hash, and reads straight from the file.
-
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§
| Family | Games | Read and extract | Write |
|---|---|---|---|
| TES3 BSA | Morrowind | yes | yes |
| TES4 BSA, versions 103 to 105 | Oblivion, Fallout 3, New Vegas, Skyrim, Skyrim SE | yes, including hash-only archives | yes |
| BA2 GNRL | Fallout 4, Fallout 76, Starfield | yes | yes |
| BA2 DX10 | Fallout 4, Fallout 76, Starfield | yes, as DDS files | from DDS files or texture metadata |
| BA2 GNMF | console textures | metadata only | no |
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.
Releases
-
2.0.0 stable
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 cratedream_archivestays at 1.1.0 on crates.io and on this repository'srustbranch, and is no longer developed on main.
org.dreamweave.dream-archive-2.0.0.jpkVerify · 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.jpkmust print the digest above. - The library is the Jess package
-
1.1.0 stable
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/archivemodule is unchanged.
dream_archive-1.1.0.crateVerify · 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.cratemust print the digest above. -
1.0.0 stable
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
luaufeature (luais the old name of the same feature) binds the crate as an l3i extension,luau::ArchiveExtension, providing@dream/archive.dream_archive::lua, themluatypes and thestandalone-luafeature are gone. - Building with the
luaufeature needs l3i's toolchain: clang, lld and cross-language thin LTO. - Every
open*in Luau returns the oneArchivetype;archive:entries()is a view of entry handles, not a table of tables. - Luau entry hashes are integers:
entry.hashandentry.folderHashreplace the old tables and fields, withhashHexandfolderHashHexfor the hex spelling. BA2 hashes aredirectoryHash,fileHashandextensionHash; sizes and offsets aresize,storedSizeandoffseton every format. - Luau integer arguments must be exact:
readEntry(1.5)is an error instead of a rounded read. bsa::Error::InvalidFileRecordFlagsis 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_hashandcontains_hashon every archive family,get_idby path, andHashFields::from_numeric.archive:readInto(entry, buffer, offset)in Luau decodes a member straight into abuffer, andarchive:getByHashandarchive:containsHashtake 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-analysisfeature. toBuffer()on every Luau builder; onlyba2.Builderhad it.dream_archive::flate2, the re-exportedflate2whoseCompressionset_zlib_leveltakes, so naming a zlib level needs noflate2dependency of your own.
Changed
- Path lookups normalize into a stack buffer instead of allocating:
containsover 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 ascontainsandreadEntrytake 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_bytesandadd_dds_filetake 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 haveDDSD_DEPTHor its caps haveDDSCAPS2_VOLUME.- A cubemap's extended
DX10DDS 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, andadd_dds_bytesandadd_dds_filerefused 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.nifmissed andmeshes/door.nif/found a member a BSA answered the other way round; hash-only TES4 lookups misseda//b/c.nif. Archives without names now hash the normalized path. - Luau
entry.nameis the part of the path after its last separator in every family. For a BA2 entry it was the whole stored path, the same asentry.path. - Luau
entry.folderis""for an entry at the root of a TES3 archive or a BA2, as it was for TES4, instead ofnil;nilmeans the archive stores no names. - The crate builds with only the
ba2feature: it now enables thelz4_flexfeature its compressor needs, which onlybsa-tes4turned on before. Builds with one BSA family alone no longer carry dead code.
Migration
Compose
dream_archive::luau::ArchiveExtensioninto the host's l3iRuntimePlan. A host that still wants thedreamArchiveglobal addsRuntimePolicy::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.crateVerify · 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.cratemust print the digest above. - The
Credits
- MaintainerDreamWeave