Asset handles
Looking an asset up by string is a runtime failure waiting to happen:
// Renames `player.png` to `player_idle.png` and this compiles fine.const entry = archive.find("textures/player.png") orelse return error.MissingAsset;The rename succeeds, the build succeeds, the tests that do not touch that asset succeed, and the failure arrives on a player’s machine. Asset handles move that failure to compile time.
The workflow
Section titled “The workflow”-
Pack your assets
Terminal window zpack pack assets/ game.zpak -
Generate the handle enum
Terminal window zpack ids game.zpak > src/assets.zigsrc/assets.zig // Generated by zpack from game.zpak. Do not edit.pub const Asset = enum(u64) {@"audio/blip.wav" = 0x40a8e93753577eb5,@"data/noise.bin" = 0xbc5d025c01217e9b,@"jimmy.png" = 0xda77d2ecbff2fabb,@"levels/level-01.json" = 0x3a43adffebb476d2,@"shaders/sprite.frag" = 0xc51bb341f35636a4,}; -
Look assets up by handle
const Asset = @import("assets.zig").Asset;fn load(archive: *const zpack.Archive, asset: Asset) ?zpack.Entry {return archive.findId(@intFromEnum(asset));}const entry = load(&archive, .@"jimmy.png") orelse return error.MissingAsset; -
Rename an asset and watch the build fail
error: enum 'Asset' has no member named 'jimy.png'Regenerate
assets.zig, fix the references the compiler points at, and move on.
What a handle actually is
Section titled “What a handle actually is”XxHash64 of the path bytes under a fixed seed:
pub const id_seed: u64 = 0x5a7061636b496473;
pub fn assetId(path: []const u8) u64 { return Hasher.hash(id_seed, path);}Three consequences worth knowing:
Nothing is stored in the archive. A handle is derived from the path, not
recorded next to it. zpack ids computes them at generation time and
findId computes them at lookup time, from the same function.
Handles survive a repack. Rebuilding the archive does not disturb them.
Regenerate assets.zig when the set of paths changes, not every build.
They are comptime-evaluable. You can write one by hand without the CLI:
const Asset = enum(u64) { @"jimmy.png" = zpack.format.assetId("jimmy.png"),};That is what example 03 does, so it stands alone without a generation step.
Collisions are a build error
Section titled “Collisions are a build error”Two paths hashing to the same u64 would make one asset unreachable. Rather
than tolerate that, both pack and Archive.open reject it:
$ zpack pack assets/ game.zpakzpack: cannot pack 'assets/': AssetIdCollisionThe failure lands on the change that introduced it, in your build, not on a player’s machine. With 64 bits, a coin-flip chance of one collision takes around five billion paths - but “unlikely” is not “handled”, so it is checked.
Keeping the enum fresh
Section titled “Keeping the enum fresh”The generated file is a build artifact. Two options:
Check it in. Simple, diffable, and a missing regeneration shows up as a compile error rather than a silent mismatch. Add a CI step that regenerates and diffs:
zpack ids game.zpak > /tmp/assets.zigdiff src/assets.zig /tmp/assets.zigGenerate it during the build. No file to forget, at the cost of a build step. See build integration.
Either way, a stale enum fails loudly: a handle for a path no longer in the
archive returns null from findId, and a path added since generation has no
enum member to reference.
When strings are still fine
Section titled “When strings are still fine”Handles are for assets your code names directly. They are not for:
- Paths built at runtime -
levels/level-{d}.jsonfor a level number - User-supplied or moddable content, which by definition is not known at compile time
- Iterating everything in a category, where you want
archive.entriesand a prefix check
find by path is a hash lookup either way. Handles buy compile-time checking,
not speed - use them where the compile-time check is worth having.