Your own art and sound: the Asset kind
Designed and built 2026-10-09, Wave 4 of the audit (design/audit-2026-10.md: "Import your own media"). Read with ADR-0002 (a game is documents plus allowed media), ADR-0005 (schemas are the contract) and ADR-0011 (assets are recipes first; files are frozen).
Why
Every sprite, model, sound and score was made from a recipe. That is the right default (it works offline, restyles in one document and costs nothing), but a person who drew their hero, bought a sound pack or hired an artist could not use any of it. The best tools let you bring your files; Gamewright has to, safely: a bundle must never carry a script or a Godot resource, a picture must never be big enough to take a phone's memory, and a store's AI disclosure has to be filled from what the person said, not guessed.
What
An Asset document (schemas/v1/asset.schema.json, assets/<slug>.asset.json) names one
media file in the pack and pins it:
file: its path and its SHA-256. Import keeps the file atassets/<slug>-<first 12 hex of its hash>.<format>: a name a person recognises in their folder, which no other file can take, so it is written once and never replaced. A changed file is a new asset (asset.changedrefuses the old one); undo finds the old file where it was.bytes, andmedia: what its bytes are, read at import and read again at every check: a picture'sformat(png, jpg, webp),widthPxandheightPx, or a sound'sformat(ogg, wav, mp3),durationSec,channelsandrateHz.source: the ledger.origin(unstated, made_by_me, made_for_me, made_by_others),author,license(SPDX or LicenseRef-*),url,ai(unstated, no_ai, ai_assisted, ai_generated),aiTool,credit. Steam's content survey asks which content AI made and with what; itch.io tags AI-generated graphics and sound; a credits page needs author and credit line. Every one of them can be filled from these fields, per file, by media kind. Unstated stays unstated: a warning (asset.source_unstated) asks for it, and nothing guesses.
What the player takes, and why (core/src/game/media.ts, the engine's GwMediaSniff):
| formats | limits | why | |
|---|---|---|---|
| pictures | PNG, JPEG (baseline, progressive), still WebP | 4096 px a side, 8 megapixels | the largest texture a phone's browser draws; 32 MB decoded |
| sounds | Ogg Vorbis, WAV (PCM 8/16/24/32-bit, 32-bit float), MP3 | 60 s, mono or stereo, 8–96 kHz | every sound is baked into a 16-bit buffer at load, on one thread in a browser |
| any file | 6,000,000 bytes | the web Studio sends a file in one 8 MB message; the web player fetches all media first |
What a file is comes from its bytes, never its name: each format is walked far enough to know it
is whole (a PNG's chunks and CRCs, a WAV's chunks, every Ogg page, every MP3 frame), and anything
else is refused by name (a GIF, an SVG, a Godot scene or resource, a script, a zip, a program, an
Opus or FLAC sound). Not now: glTF/GLB. Godot's GLTFDocument loads one without scripts, but it
follows external URIs, decodes embedded textures of any size and runs registered extensions; the
3D look path is Model recipes, which take no texture yet. It waits until both are designed.
Files only from inside the pack. readMediaFile refuses a path with an empty, ., .. or
hidden segment, a backslash, or a link whose real path leaves the pack, and reads a file only once
its size is known to be within the limit. Bake reads through it too, so a link in a pack's folder
can no longer carry a file from outside into a bundle (an Item's icon could, before). The player
checks again (GwMedia): the bytes must hash to the Asset's pin, be what it says, and name a size
within the limits before a decoder sees them; the bundle loader refuses a "png" that is not one
(bundle.media_content).
Using an asset. Two builders, so a Sprite and a Sound stay what they were:
sprite.sheet(kit.sprite): an imported picture cut into frames of one size, each clip a run of frames along a row (clips: [{name, row, column, frames, fps, loop}]), the feet at the bottom middle unlesspivotXPx/pivotYPxsay,flipfor art that faces left,pixelsPerTilefor its size in a level. Every 2D view draws it as it draws a baked sheet (GwSpriteSheet, now with a first column and a texel scale): the platformer's bodies by what they do, kit.ui's sprite widget, the Studio's previews and room look. The pixels are the person's: no palette snap.sound.file(kit.audio): an imported sound decoded by Godot's own decoders (data, never code) a chunk a step at the file's own rate, for as long as its Asset says and never past 60 s, then baked as every Sound is (high-pass, a one-shot's trim and fade, the mix sheet's gain), so it sits in the mix and plays as an AudioStreamWAV in a browser's sample mode too.varymakes variants differ in pitch and length.
So "bring your own art and sound" is real for every 2D game (the platformer, a visual novel's portraits, any Screen's sprite widget) and "your own sound" for every game, 3D included. Left for later, each needing a design of its own: a picture as a Style's backdrop or a 3D texture, glTF models, music longer than a minute (streaming), a picture as a grid game's tiles, replacing a file in place (drop on its Asset), and a file picker in the web Studio.
Importing. The person's own change, applied at once like an edit in the Inspector, so Ctrl+Z
takes it back (assets.import over the session protocol; core/src/session/assets.ts):
- the bytes come as base64; their size is told from the text before decoding;
- they are sniffed and checked against the limits; a refusal writes nothing;
- the same bytes already in the game are not imported twice: the result names that Asset;
- the file is written at
assets/<slug>-<hash12>.<format>, then one change set writes its Asset and, unless the person says not, a Sprite (sprite.sheet, when the game runs kit.sprite) or a Sound (sound.file) that uses it, ids made free with _2, _3 (a template'ssnd.jumpis never taken over); if the change is refused, the new file goes again; - a pack that is its own git repository commits the file with its Asset.
The Studio imports a file dropped on its window or chosen with File ▸ Import… (Ctrl+Shift+I, the
palette, or the last row of the project's Look and sound, "Import your own files…"). The new file opens in the
Inspector with its preview (a picture at whole-pixel zoom on a checkerboard, or a sound with its
waveform and Play) and its Source fields to fill. In the browser there is no dialog for a game on
the host: Import… says to drop the file, which goes over the same session through the host
(assets.import counts as a write against the person's storage). gamewright import does the
same from a terminal, with --origin, --ai, --license, --author and --url.
The Director sees the person's files in game_summary (assets: what each is, what uses
it), the picture itself through get_document, and both builders in the vocabulary. The playbook
says to prefer the person's files and never to name one the game lacks. Assets are the person's:
no proposal may add, change or remove one (asset.person_only, at proposal and again at
approval), and an Asset whose file isn't there fails L0, so none can be made up.
Looks, not play. An Asset is a view-only kind (stages.ts); the simulation never reads one or
its files (tools/check-presentation-reads.sh), and media reach views through the stage
(stage.service("media")), never through the simulation's content.
Proof
- Core:
core/test/assets.test.ts(sniffing every format and refusing others by name, limits, damaged files, paths and links out of the pack, the L0 checks, bake's media by hash, import and undo, already-imported, refusals that write nothing, the git commit, the Director never writing one and seeing the picture) andcore/test/session/assets.test.ts(the protocol, every message checked against session.schema.json). Each guard was put back and its test seen red. - Engine:
engine/tests/runtime/gw_media_test.gd,kits/sprite/gw_sprite_import_test.gd,kits/audio/gw_sound_import_test.gd,player/gw_player_assets_test.gd(the asset_mini bundle: the hero drawn from the imported sheet by what it does, the jump baked from the WAV), andstudio/gw_studio_import_test.gd(a drop with the real core, the preview, a refusal, Ctrl+Z). - Watched:
tools/studio/import-scenario.sh(frames of a drop on a copy of Burrow Run, the sheet cut and given to the hero), then that game baked and its look tour shot in the player.