# asset

`import "github.com/matjam/bunyip/asset"`

Package asset finds and loads a game's files. Sources are loose directories while developing, pack files when shipping, files embedded in the binary with go:embed, or any mix, with earlier sources taking precedence so a modder's or developer's copy overrides the packed or embedded one. Open takes directory and pack paths; OpenFS also takes an io/fs.FS. FS implements io/fs.FS with merged directories; loaders also accept embed.FS, os.DirFS and other fs.FS values directly. The one-call loaders (Image, Texture, Atlas, Aseprite, Font, Sound, Music, Model, Tracker, Scene, Prefab) read and decode an asset into an engine object. A Loader decodes assets on worker goroutines for loading screens, a Watcher reports loose files that change on disk, and a Reloader swaps a changed file's texture or shader into the objects a game already holds, so hot reload needs no bookkeeping in the game. GPU loaders and Reloader methods run on the rendering goroutine. Closing FS releases pack handles, not loaded resources. Graphics owns uploaded GPU resources; Destroy releases them early. The caller closes loaded music and other resources with independent lifetimes.

## Variables

<a id="ErrNotFound"></a>

```go
var ErrNotFound = fs.ErrNotExist
```

ErrNotFound is returned for names no source holds.

## Functions

<a id="Aseprite"></a>

### Aseprite

```go
func Aseprite(g *gfx.Graphics, fsys fs.FS, name string, opts gfx.AsepriteOptions, texOpts gfx.TextureOptions) (*gfx.Aseprite, error)
```

Aseprite reads an .aseprite or .ase file, composites each frame from its visible layers, uploads the packed image and binds an atlas: one call where a game would otherwise parse, pack, upload and bind by hand. The atlas is on the result's Atlas field, its frames are named by number and its tags play through Atlas.Animation; the result also carries the file's layers, tags, slices and palette.

<a id="Atlas"></a>

### Atlas

```go
func Atlas(g *gfx.Graphics, fsys fs.FS, name string, opts gfx.TextureOptions) (*gfx.Atlas, error)
```

Atlas reads a TexturePacker or Aseprite JSON atlas, loads the image it names from the same directory, uploads it and binds the frames: one call where a game would otherwise parse, load and bind by hand.

<a id="Emitter"></a>

### Emitter

```go
func Emitter(g *gfx.Graphics, fsys fs.FS, name string, opts gfx.TextureOptions) (particle.Emitter, error)
```

Emitter reads a particle emitter saved as JSON and loads the texture it names, from the same directory as the emitter file. An emitter naming no texture comes back drawing plain quads. Destroy the texture with the rest of the game's resources; it is the returned emitter's Texture.

<a id="Font"></a>

### Font

```go
func Font(g *gfx.Graphics, fsys fs.FS, name string, size float32, opts gfx.FontOptions) (*gfx.Font, error)
```

Font reads a TTF or OTF file and prepares a bitmap atlas at size.

<a id="Image"></a>

### Image

```go
func Image(fsys fs.FS, name string) (image.Image, error)
```

Image reads and decodes a PNG or JPEG.

<a id="Model"></a>

### Model

```go
func Model(g *gfx.Graphics, fsys fs.FS, name string) (*gfx.Model, error)
```

Model reads a .gltf or .glb file and uploads it. External buffers and images are read through the same FS relative to the model's directory.

<a id="Music"></a>

### Music

```go
func Music(m *audio.Mixer, fsys fs.FS, name string, loop bool) (*audio.Music, error)
```

Music reads a WAV, Ogg Vorbis or MP3 file and opens it for streaming. The encoded bytes stay in memory; decoding happens as it plays.

<a id="Pack"></a>

### Pack

```go
func Pack(dir, out string) error
```

Pack writes every file under dir into a pack file at out. Files that are already compressed (images, audio, video) are stored as they are; the rest are deflated.

<a id="Prefab"></a>

### Prefab

```go
func Prefab(fsys fs.FS, name string) (*ecs.Prefab, error)
```

Prefab reads a prefab document, for the ecs.PrefabLibrary a scene's prefab references are resolved against. Every component type the file names must be registered first.

<a id="SDFFont"></a>

### SDFFont

```go
func SDFFont(g *gfx.Graphics, fsys fs.FS, name string, size float32, opts gfx.FontOptions) (*gfx.Font, error)
```

SDFFont reads a TTF or OTF file and prepares a scalable font.

<a id="Scene"></a>

### Scene

```go
func Scene(fsys fs.FS, name string) (*ecs.Scene, error)
```

Scene reads a scene document, ready for ecs.World.Instantiate. The component types the scene names are checked when it is instantiated, not here, so a scene loads before the game registers them.

<a id="Sound"></a>

### Sound

```go
func Sound(m *audio.Mixer, fsys fs.FS, name string) (*audio.Sound, error)
```

Sound reads and decodes a WAV, Ogg Vorbis or MP3 clip into the mixer's format.

<a id="Texture"></a>

### Texture

```go
func Texture(g *gfx.Graphics, fsys fs.FS, name string, opts gfx.TextureOptions) (*gfx.Texture, error)
```

Texture decodes an image and uploads it. A name ending in .ktx2 is a compressed texture from bunyip-tex: its blocks and its mip levels go to the GPU as they stand, without being decoded or downsampled first.

<a id="Tracker"></a>

### Tracker

```go
func Tracker(fsys fs.FS, name string) (*tracker.Module, error)
```

Tracker reads and parses a MOD, S3M, XM or IT module.

## Types

<a id="FS"></a>

### FS

```go
type FS struct {
	// contains filtered or unexported fields
}
```

FS resolves names against its sources in order and implements fs.FS and fs.ReadFileFS. Open and ReadFile use standard io/fs paths. Read, Exists, Path and List retain their cleaned-name convenience syntax.

<a id="Open"></a>

#### Open

```go
func Open(sources ...string) (*FS, error)
```

Open takes directories and pack files, searched in the order given. A missing source is an error. Check optional source paths with os.Stat before opening; FS.Exists checks asset names after an FS is open. OpenFS accepts embedded file systems as well.

<a id="OpenFS"></a>

#### OpenFS

```go
func OpenFS(sources ...Source) (*FS, error)
```

OpenFS takes sources of any kind, searched in the order given. A missing directory or pack is an error.

<a id="FS.Close"></a>

#### FS.Close

```go
func (f *FS) Close()
```

Close releases pack files.

<a id="FS.Exists"></a>

#### FS.Exists

```go
func (f *FS) Exists(name string) bool
```

Exists reports whether some source holds the name.

<a id="FS.List"></a>

#### FS.List

```go
func (f *FS) List(prefix string) []string
```

List returns visible file names under prefix, sorted and without duplicates. Unreadable directories are omitted; use fs.WalkDir when errors need to be reported.

<a id="FS.Open"></a>

#### FS.Open

```go
func (f *FS) Open(name string) (file fs.File, err error)
```

Open opens a file or merged directory using io/fs paths. Earlier sources win when names conflict; directories merge their children. A file hides all lower-priority entries beneath its name. Only missing entries fall through to later sources; other errors are returned. The returned handle belongs to the caller and must be closed before FS.

<a id="FS.Path"></a>

#### FS.Path

```go
func (f *FS) Path(name string) string
```

Path returns the on-disk path of a loose file, or "" when the name resolves to a pack or fs.FS entry or nothing. Watchers use it.

<a id="FS.Read"></a>

#### FS.Read

```go
func (f *FS) Read(name string) ([]byte, error)
```

Read returns the named file's contents. Names use forward slashes relative to the source roots, like "sprites/hero.png". Unlike ReadFile, this convenience method cleans dot components and backslashes first.

<a id="FS.ReadFile"></a>

#### FS.ReadFile

```go
func (f *FS) ReadFile(name string) ([]byte, error)
```

ReadFile reads a file using io/fs paths and errors. The result belongs to the caller. Directories cannot be read as files.

<a id="Handle"></a>

### Handle

```go
type Handle[T any] struct {
	// contains filtered or unexported fields
}
```

Handle is a pending or finished load.

<a id="Handle.Get"></a>

#### Handle.Get

```go
func (h *Handle[T]) Get() (T, error)
```

Get waits for the load and returns its result.

<a id="Handle.Ready"></a>

#### Handle.Ready

```go
func (h *Handle[T]) Ready() bool
```

Ready reports whether the load has finished, successfully or not.

<a id="Handle.Value"></a>

#### Handle.Value

```go
func (h *Handle[T]) Value() (v T, err error, ok bool)
```

Value returns the result without waiting; ok is false until ready.

<a id="Loader"></a>

### Loader

```go
type Loader struct {
	// contains filtered or unexported fields
}
```

Loader reads and decodes assets on worker goroutines so a loading screen can keep drawing. Decoding produces CPU-side data (an image, decoded audio, a parsed model); creating GPU resources from it happens on the main thread once a handle is ready. Close finishes accepted loads and joins the workers before returning; the filesystem can then be closed. Wait joins currently submitted work without closing the loader; stop submitting loads before calling Wait.

<a id="NewLoader"></a>

#### NewLoader

```go
func NewLoader(fsys fs.FS, workers int) *Loader
```

NewLoader starts workers reading from fs; workers of zero means one per CPU.

<a id="Loader.Close"></a>

#### Loader.Close

```go
func (l *Loader) Close()
```

Close stops submission and waits for accepted loads and all workers. Repeated and concurrent calls are safe. A blocked reader or decoder must return before Close can finish. Do not call Close from a decoder.

<a id="Loader.Load"></a>

#### Loader.Load

```go
func (l *Loader) Load[T any](name string, decode func(data []byte) (T, error)) *Handle[T]
```

Load reads name and decodes it on a worker. Submission can block when the 256-entry queue is full. A load submitted after shutdown starts returns a ready handle with fs.ErrClosed. Decode must not create GPU resources or call Close on its own loader.

<a id="Loader.Progress"></a>

#### Loader.Progress

```go
func (l *Loader) Progress() (done, total int)
```

Progress reports loads finished and requested so far, for a bar.

<a id="Loader.Wait"></a>

#### Loader.Wait

```go
func (l *Loader) Wait()
```

Wait blocks until every requested load has finished.

<a id="Reloader"></a>

### Reloader

```go
type Reloader struct {
	// contains filtered or unexported fields
}
```

Reloader keeps what a game loaded in step with the files it came from, so a texture repainted or a shader recompiled while the game runs appears the moment it is saved. To use one, load through it instead of the package's loaders and call Reload once a frame:

	rel := asset.NewReloader(ctx.Gfx, fs, 0)
	tex, err := rel.Texture("sprites/hero.png", gfx.TextureOptions{})
	// ... hand tex to a material or draw it as a sprite ...

	func (g *game) Update(ctx *engine.Context) error {
		names, err := g.rel.Reload()
		if err != nil {
			ctx.Log.Warn("reload failed", "err", err)
		}
		for _, n := range names {
			ctx.Log.Info("reloaded", "asset", n)
		}
		return nil
	}

Everything it loads keeps the pointer it handed back. A texture's image is swapped in place, so every material, sprite and shader slot that names it draws the new pixels without being told, even at a new size; a shader's pipelines are rebuilt, so every draw through it runs the new program. Nothing a game holds goes stale, so a reload needs no bookkeeping of its own.

Only loose files change: a name that resolves into a pack file or an embedded file system is loaded once and never watched, so a shipped game pays for the polling goroutine and nothing else. Close stops it.

Models and environments are not reloaded. Swapping a glTF file's contents would give back different meshes, a different skeleton and different animation clips, which every AnimPlayer, mesh pointer and node index the game holds refers to. A gfx.Environment is built by prefiltering a panorama into a cube map, and a reflection probe bakes and owns one of its own, so replacing the image behind one would mean rebuilding every level of that cube while the game runs. A game that wants either loads it again and rebinds what pointed at the old one.

<a id="NewReloader"></a>

#### NewReloader

```go
func NewReloader(g *gfx.Graphics, fs *FS, interval time.Duration) *Reloader
```

NewReloader watches fs for changes and reloads through g. interval is how often loose files are checked; zero means half a second. Close it when the game ends.

<a id="Reloader.Close"></a>

#### Reloader.Close

```go
func (r *Reloader) Close()
```

Close stops watching. The assets it loaded are the game's to destroy.

<a id="Reloader.MeshShader"></a>

#### Reloader.MeshShader

```go
func (r *Reloader) MeshShader(name string) (*gfx.Shader, error)
```

MeshShader is Shader for a shader that draws meshes rather than sprites.

<a id="Reloader.Reload"></a>

#### Reloader.Reload

```go
func (r *Reloader) Reload() ([]string, error)
```

Reload swaps in every watched file that changed since the last call and returns the names it reloaded. Call it once a frame from Update. A file that fails to read or decode keeps the asset the game already has and its error is returned, while the other names still reload, so one bad save does not stop the rest; the file is tried again the next time it is written.

<a id="Reloader.Shader"></a>

#### Reloader.Shader

```go
func (r *Reloader) Shader(name string) (*gfx.Shader, error)
```

Shader compiles a sprite shader from a .spv file written by bunyip-shader and rebuilds it when the file changes. Its images and uniforms are kept across a reload.

<a id="Reloader.Texture"></a>

#### Reloader.Texture

```go
func (r *Reloader) Texture(name string, opts gfx.TextureOptions) (*gfx.Texture, error)
```

Texture loads an image or a KTX2 texture and reloads it in place when the file changes. It is asset.Texture with the watching added, so the options and the errors are the same.

<a id="Reloader.Watch"></a>

#### Reloader.Watch

```go
func (r *Reloader) Watch(name string, reload func(data []byte) error)
```

Watch calls reload with the file's new contents whenever the named file changes, for an asset this package has no loader for: a level, a table of tuning values, a palette. The call happens inside Reload, on the goroutine that calls it, so it may touch the game and the GPU. Several targets may watch one name and each is called in turn.

<a id="Source"></a>

### Source

```go
type Source struct {
	// contains filtered or unexported fields
}
```

Source is a place an FS looks for files. Build one with Dir, PackFile or FSSource and pass it to OpenFS.

<a id="Dir"></a>

#### Dir

```go
func Dir(path string) Source
```

Dir is a directory of loose files.

<a id="FSSource"></a>

#### FSSource

```go
func FSSource(fsys fs.FS) Source
```

FSSource is any io/fs.FS, typically an embed.FS so a game ships its assets inside the binary. Names resolve relative to the FS root, so pass fs.Sub(embedded, "assets") when the embed directive names a directory. Path returns "" for its files and a Watcher ignores them.

Example:

```go
package main

import (
	"fmt"
	"os"
	"testing/fstest"

	"github.com/matjam/bunyip/asset"
)

func main() {
	// A game embeds its assets with go:embed and opens them behind an
	// optional loose directory so edits on disk win while developing.
	// Any io/fs.FS works; a MapFS stands in for the embed.FS here.
	embedded := fstest.MapFS{
		"text/intro.txt": {Data: []byte("Once upon a time")},
		"text/end.txt":   {Data: []byte("The end")},
	}
	sources := []asset.Source{asset.FSSource(embedded)}
	if _, err := os.Stat("assets"); err == nil {
		sources = append([]asset.Source{asset.Dir("assets")}, sources...)
	}
	fs, err := asset.OpenFS(sources...)
	if err != nil {
		panic(err)
	}
	defer fs.Close()
	data, _ := fs.Read("text/end.txt")
	fmt.Println(string(data))
	fmt.Println(fs.List("text"))
	fmt.Printf("%q\n", fs.Path("text/end.txt"))
}
```

Output:

```
The end
[text/end.txt text/intro.txt]
""
```

<a id="PackFile"></a>

#### PackFile

```go
func PackFile(path string) Source
```

PackFile is a pack written by Pack or bunyip-pack.

<a id="Watcher"></a>

### Watcher

```go
type Watcher struct {
	// contains filtered or unexported fields
}
```

Watcher polls loose files for changes so a running game can reload a texture or shader the moment it is saved. Packed files never change, so names that resolve into a pack are ignored.

Add resolves each name to its file on disk once. A poll then stats that file, plus each directory a copy of the name could appear in ahead of it: the name's directory in every loose source up to the one that holds it, or in every loose source when none does. A name is resolved again only when one of those directories changes, so a copy added to or removed from an overlaying source is still noticed. The poll runs without the lock Changed takes, so a game calling Changed every frame never waits for one.

<a id="NewWatcher"></a>

#### NewWatcher

```go
func NewWatcher(fs *FS, interval time.Duration) *Watcher
```

NewWatcher polls every interval (zero means half a second).

<a id="Watcher.Add"></a>

#### Watcher.Add

```go
func (w *Watcher) Add(names ...string)
```

Add starts watching names; loading an asset and adding it here is the usual pair.

<a id="Watcher.Changed"></a>

#### Watcher.Changed

```go
func (w *Watcher) Changed() []string
```

Changed returns the names modified since the last call, in the order they were first noticed, and clears them. A name appears once however many times its file changed, because a save can change a file more than once (truncate, write, set the time) and one reload of the latest contents covers them all. Call it once per frame.

<a id="Watcher.Close"></a>

#### Watcher.Close

```go
func (w *Watcher) Close()
```

Close stops polling.

## Examples

Example:

```go
package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/matjam/bunyip/asset"
)

func main() {
	// A directory of loose files while developing; ship a pack file built
	// with bunyip-pack and open both, directory first, so loose files win.
	dir, _ := os.MkdirTemp("", "asset-example")
	defer os.RemoveAll(dir)
	os.MkdirAll(filepath.Join(dir, "text"), 0o755)
	os.WriteFile(filepath.Join(dir, "text", "intro.txt"), []byte("Once upon a time"), 0o644)
	asset.Pack(dir, filepath.Join(dir, "..", "example.pak"))
	defer os.Remove(filepath.Join(dir, "..", "example.pak"))

	fs, err := asset.Open(dir, filepath.Join(dir, "..", "example.pak"))
	if err != nil {
		panic(err)
	}
	defer fs.Close()
	data, _ := fs.Read("text/intro.txt")
	fmt.Println(string(data))
	fmt.Println(fs.List(""))

	// Decode on worker goroutines; poll Ready from the game loop, or Wait.
	loader := asset.NewLoader(fs, 0)
	defer loader.Close()
	upper := loader.Load("text/intro.txt", func(b []byte) (string, error) { return strings.ToUpper(string(b)), nil })
	v, _ := upper.Get()
	fmt.Println(v)
}
```

Output:

```
Once upon a time
[text/intro.txt]
ONCE UPON A TIME
```
