Asset Archiving
Terrarum uses the TerranVirtualDisk (TEVD) Clustered format to package game assets into a single archive file (assets.tevd) for distribution builds. Assets are read directly from the archive at runtime — no extraction step is needed.
In development, the engine reads loose files from ./assets/. The switch is automatic based on whether assets.tevd exists next to the game JAR.
Build time: assets/ → assets_release/ → assets.tevd
Runtime: assets.tevd opened as ClusteredFormatDOM → files served on demand
Overview
Two Modes of Operation
| Development | Distribution | |
|---|---|---|
| Assets source | ./assets/ directory |
./assets.tevd archive |
| Detection | No assets.tevd present |
assets.tevd exists next to the JAR |
| FileHandle type | Gdx.files.internal(...) |
ClustfileHandle (backed by Clustfile) |
| Used by | Developers running from IDE | Release builds shipped to players |
Key Classes
AssetCache— Central accessor; opens the archive on startup, providesFileHandleinstancesClustfileHandle— GDXFileHandlesubclass backed by a virtual disk fileAssetArchiveBuilder— Build-time tool that createsassets.tevdfromassets_release/ModMgr— Module loader; usesAssetCacheto resolve internal mod assets transparently
Build Pipeline
Step 1: Prepare Release Assets
The assets_release/ directory is a processed copy of assets/, stripped of source-only files. This step is handled by buildapp/make_assets_release.sh.
Step 2: Create the Archive
Run make assets from the buildapp/ directory, or invoke the builder directly:
java -cp TerrarumBuild.jar:lib/TerranVirtualDisk.jar \
net.torvald.terrarum.AssetArchiveBuilderKt \
assets_release out/assets.tevd
The builder (AssetArchiveBuilder.kt) performs:
- Scan — Walks
assets_release/to count files, directories, and total size - Allocate — Creates a new TEVD archive with sufficient capacity (clusters + FAT entries)
- Pre-grow FAT — Expands the File Allocation Table upfront to avoid costly mid-import growth
- Import — Recursively imports all files and directories via
Clustfile.importFrom() - Trim — Removes unused trailing clusters to minimise file size
Build Script
The build script (buildapp/make_assets_archive.sh) assembles the classpath from the project JAR and all library JARs, then invokes AssetArchiveBuilderKt:
# Simplified:
CP="../out/TerrarumBuild.jar"
for jar in ../lib/*.jar; do CP="$CP:$jar"; done
java -cp "$CP" net.torvald.terrarum.AssetArchiveBuilderKt "$SRCDIR" "$OUTDIR/assets.tevd"
Runtime Loading
Initialisation
AssetCache.init() is called early in App.main():
AssetCache.init()
// or with an explicit path:
AssetCache.init("/path/to/custom.tevd")
If ./assets.tevd exists, the archive is opened as a read-only ClusteredFormatDOM. Otherwise, the engine falls back to reading loose files from ./assets/.
Getting File Handles
All asset access should go through AssetCache or ModMgr:
// Via AssetCache (for paths relative to assets root):
val handle: FileHandle = AssetCache.getFileHandle("mods/basegame/audio/music/title.ogg")
// Via ModMgr (for module-relative paths — preferred for mod content):
val handle: FileHandle = ModMgr.getGdxFile("basegame", "audio/music/title.ogg")
Both return a standard GDX FileHandle. In distribution mode this is a ClustfileHandle; in development mode it is a regular Gdx.files.internal(...) handle. Callers do not need to know which.
What Works Transparently
Because ClustfileHandle extends FileHandle, all standard GDX asset loading works without changes:
Gdx.audio.newMusic(fileHandle)— Streaming audioTexture(fileHandle)— Texture loadingPixmap(fileHandle)— Image processingJsonReader().parse(fileHandle.readString())— JSON dataProperties().load(fileHandle.read())— Java properties filesfileHandle.readBytes()— Raw binary datafileHandle.list()— Directory listingfileHandle.child("name")— Child file accessfileHandle.sibling("name")— Sibling file access
ClustfileHandle
ClustfileHandle bridges GDX's FileHandle API to TerranVirtualDisk's Clustfile:
class ClustfileHandle(private val clustfile: Clustfile) : FileHandle() {
override fun read(): InputStream = ClustfileInputStream(clustfile)
override fun readBytes(): ByteArray = clustfile.readBytes()
override fun exists(): Boolean = clustfile.exists()
override fun length(): Long = clustfile.length()
override fun isDirectory(): Boolean = clustfile.isDirectory
override fun name(): String = clustfile.name
override fun path(): String = clustfile.path
override fun list(): Array<FileHandle> = clustfile.listFiles()?.map { ClustfileHandle(it) }?.toTypedArray() ?: arrayOf()
override fun child(name: String): FileHandle = ClustfileHandle(Clustfile(clustfile.DOM, "$path/$name"))
override fun sibling(name: String?): FileHandle = ClustfileHandle(Clustfile(clustfile.DOM, "$parent/$name"))
override fun parent(): FileHandle = ClustfileHandle(clustfile.parentFile ?: Clustfile(clustfile.DOM, "/"))
// ...
}
The read() method returns a ClustfileInputStream that reads data from the virtual disk's clusters or inline FAT entries, supporting all standard InputStream operations including mark(), reset(), and skip().
AssetCache API Reference
| Method | Returns | Description |
|---|---|---|
init() |
— | Opens ./assets.tevd if present |
init(path) |
— | Opens a specific .tevd archive |
isDistribution |
Boolean |
Whether running from an archive |
getFileHandle(path) |
FileHandle |
GDX handle for an asset (relative to assets root) |
getClustfile(path) |
Clustfile |
Raw virtual disk file handle (distribution only) |
resolve(path) |
String |
Filesystem path (development only; throws in distribution) |
dispose() |
— | Closes the archive |
TEVD Archive Format
The archive uses TerranVirtualDisk's Clustered format — the same format used for savegames. Key characteristics:
- Cluster size — 4096 bytes
- FAT entries — 256 bytes each, 16 per cluster
- Inline files — Files under ~2 KB are stored directly in FAT entries (no cluster allocation)
- Directory listings — Stored as arrays of 3-byte entry IDs, sorted by filename
- Read-only at runtime — The archive is opened with
RandomAccessFile(file, "r")
For format details, see the TerranVirtualDisk repository.
For Module Developers
If you are developing a module:
- Internal modules (shipped with the game) — Assets are inside
assets.tevdundermods/<module>/. UseModMgr.getGdxFile(module, path)to access them. - External modules (installed by players) — Assets live on the real filesystem under
~/.Terrarum/Modules/<module>/. These are not affected by the archive system.ModMgr.getGdxFile()returns a regularFileHandlefor them.
No changes to module code are needed. ModMgr.getGdxFile() handles both cases transparently.
Troubleshooting
Common Issues
- "No archive found, using loose assets" —
assets.tevdis not next to the game JAR. This is normal during development. - "Cannot seek to inlined cluster" — A
ClustfileInputStreamread went past the end of file. Ensure callers respect the stream'savailable()and return values. - Files not found in archive — Verify the file exists in
assets_release/before building the archive. UseAssetArchiveBuilderwith a small test directory to debug.
Verifying Archive Contents
You can inspect the archive by writing a small test program:
val dom = ClusteredFormatDOM(RandomAccessFile(File("assets.tevd"), "r"))
val root = Clustfile(dom, "/")
root.listFiles()?.forEach { println(it.path) }
dom.dispose()