1
Asset Archiving
minjaesong edited this page 2026-02-28 10:53:51 +09:00

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, provides FileHandle instances
  • ClustfileHandle — GDX FileHandle subclass backed by a virtual disk file
  • AssetArchiveBuilder — Build-time tool that creates assets.tevd from assets_release/
  • ModMgr — Module loader; uses AssetCache to 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:

  1. Scan — Walks assets_release/ to count files, directories, and total size
  2. Allocate — Creates a new TEVD archive with sufficient capacity (clusters + FAT entries)
  3. Pre-grow FAT — Expands the File Allocation Table upfront to avoid costly mid-import growth
  4. Import — Recursively imports all files and directories via Clustfile.importFrom()
  5. 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 audio
  • Texture(fileHandle) — Texture loading
  • Pixmap(fileHandle) — Image processing
  • JsonReader().parse(fileHandle.readString()) — JSON data
  • Properties().load(fileHandle.read()) — Java properties files
  • fileHandle.readBytes() — Raw binary data
  • fileHandle.list() — Directory listing
  • fileHandle.child("name") — Child file access
  • fileHandle.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.tevd under mods/<module>/. Use ModMgr.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 regular FileHandle for them.

No changes to module code are needed. ModMgr.getGdxFile() handles both cases transparently.

Troubleshooting

Common Issues

  • "No archive found, using loose assets"assets.tevd is not next to the game JAR. This is normal during development.
  • "Cannot seek to inlined cluster" — A ClustfileInputStream read went past the end of file. Ensure callers respect the stream's available() and return values.
  • Files not found in archive — Verify the file exists in assets_release/ before building the archive. Use AssetArchiveBuilder with 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()