Package Resources

SimVX accepts game assets (textures, audio, meshes, glTF scenes, config files) as filesystem paths or as Python-package handles. The canonical handle for assets that ship inside a package is :class:simvx.core.Resource, which resolves to a file via :mod:importlib.resources.

Why importlib.resources?

  • One naming scheme. Assets live in Python packages (a directory with an __init__.py). The same Resource(package, name) handle works whether the package is editable, installed from a wheel, frozen into a zipapp, or served from a Pyodide bundle.

  • Refactor-safe. Renaming a package via your IDE/LSP keeps all Resource("my_game.assets", ...) references in sync — no separate UID database to migrate.

  • No sidecar files. No .import, .uid, or .meta files cluttering the tree, no manifest to regenerate. The Python module graph is the manifest.

  • No codegen. Scenes stay .py (see project rule) and asset references stay first-class Python objects.

The Resource helper

from simvx.core import Resource

helmet = Resource("my_game.assets.models", "helmet.gltf")

helmet.path          # PosixPath('/.../my_game/assets/models/helmet.gltf')
helmet.read_bytes()  # b'...'

with helmet.open() as fh:
    data = fh.read()

Resource(package, name) is lazy — construction never touches the filesystem. Resolution happens on first access of .path / .read_bytes() / .open(). Errors are strict by default:

Failure

Exception

Empty package or name

ValueError

Package cannot be imported

ModuleNotFoundError

File missing inside package

FileNotFoundError

Resource instances are equal by (package, name) and hashable, so they work as dict keys and in sets.

Marking a directory as a resource package

A directory becomes a Python package — and therefore a Resource lookup target — by adding an empty __init__.py:

my_game/
    __init__.py
    main.py
    assets/
        __init__.py          <-- makes "my_game.assets" importable
        models/
            __init__.py      <-- makes "my_game.assets.models" importable
            helmet.gltf
            helmet.bin

Then anywhere in your game:

from simvx.core import Resource

HELMET = Resource("my_game.assets.models", "helmet.gltf")

Worked example: the model viewer demo

packages/graphics/examples/3d_model_viewer.py loads the Khronos DamagedHelmet sample. The relevant pieces:

from simvx.core import Resource
from simvx.graphics.assets.scene_import import import_gltf

HELMET_GLTF = Resource("DamagedHelmet", "DamagedHelmet.gltf")

# ...inside ready()...
model = import_gltf(str(HELMET_GLTF.path))
self.add_child(model)

The assets/DamagedHelmet/ directory ships an __init__.py, so importlib.resources.files("DamagedHelmet") resolves to that folder. The glTF parser opens the .gltf file at the resolved path and pulls the sibling .bin and .jpg textures via the relative URIs embedded inside the glTF, which “just works” because Resource.path returns a real on-disk Path.

Filesystem paths

For assets that don’t ship inside a Python package (e.g. user-supplied files, content downloaded at runtime, project-relative working copies), just pass a str or pathlib.Path:

from pathlib import Path
from simvx.core import AudioStream, Mesh

AudioStream("music/theme.ogg")                # relative to CWD / project root
AudioStream(Path.home() / "tracks" / "x.ogg") # absolute Path
Mesh.from_obj("models/ship.obj")              # filesystem OBJ

Strings are filesystem paths only — there is no URI scheme to parse and no ambiguity between “package” and “path” syntax.

Accepted source types

AudioStream, Mesh.from_obj, ResourceLoader.load_threaded_request, and resolve_asset_path all accept the same union:

Source

When to use

str / pathlib.Path

Filesystem path (absolute or project-root-relative).

Resource(package, name)

Asset shipped inside a Python package.

importlib.resources.Traversable

Raw importlib.resources.files(pkg) / name handle.

Anything else raises TypeError.