Game-facing identity handles. The only way game code (Zig / Kawa) references things.
| Type | Scheme | Use for | Not for |
|---|---|---|---|
AssetRef | asset://… stem + AssetKind | Cooked assets: texture, model, material, model_doc, audio, collision, script, shader, font, sprite_atlas, prefab, particle, animation_graph | JSON tables, OS paths, cooked suffixes, skybox (use a .texture ref) |
ContentRef | content:// (dirs allowlist, AssetStore only) · writable:// / project:// (FS) | Tables, configs, saves | Scenes, cooked assets |
SceneRef | relative stem scenes/foo (runtime: cooked .shinscene; authoring .json Debug only) | Level load via AssetStore | Content tables, free-form content |
PrefabRef | relative stem prefabs/foo (runtime: cooked .shinprefab; authoring .prefab.json Debug only) | Prefab retain / spawn | Scenes, loose content |
ActorRef | generation handle (EntityId alias) | Live actors (find / send / bind) | Strings as “entities” |
VoiceHandle | generation-checked slot | Live audio voice from hi.audio().playCue / crossfade | Cue paths (use AssetRef) |
URI forms (full rules: Assets and Shinra — Asset URIs and packs):
| Form | Meaning |
|---|---|
asset://<pack>/<logical> | Explicit pack; that pack must be retained by the scene root packs array |
asset://./<logical> | Unscoped absolute logical path; product catalog maps path → pack; that pack must be retained. Not FS-relative |
There is no pack-search form. Seal enforces one path → one pack.
Path identity (engine internals): Shinra pipeline events may still emit cooked suffixes. Compare stems with asset_kind.stemsEqual / asset_deps.stemsEqual (not bare string/pathsEqual). Scenes use scene_ref.stemOf / withAuthoringJson / withCooked for store and editor disk loads.
Shaders are AssetKind.shader but identity is a package id (gbuffer_wacky, _engine/gbuffer), not a file stem. Platform expand lives in graphics/material/shader_artifact.zig (Metal one .metallib; D3D12 _vs_/_ps_/_cs_.cso). Materials still set shader.package; runtime acquires expanded store paths only for the synchronous native copy/compile and then releases them. Session launch binds AssetStore; device-init must follow launch.
Not refs (plain names): archetype, input action, message name, physics preset, audio bus.
Engine-internal native atoms (AudioHandle, RenderHandle, PhysicsHandle in src/hikari/src/native/handle.zig) back subsystem tables — games use AssetRef for cue paths and VoiceHandle for live voices.
| API surface | Type |
|---|---|
RenderUpdate.mesh / .material / .albedo | AssetRef (kinds .model / .material / .texture) |
setSkyboxTexture / attachSessionScript / defineActor(.{ .script = … }) | AssetRef (skybox = .texture; script = .script) |
paths.read / json.parseRef | ContentRef (content:// first segment ∈ project content_dirs) |
requestSceneLoad (replace / additive) | SceneRef |
retainPrefab / spawnPrefab | PrefabRef |
find / findBySceneId / findByName / raycast hit / spawn return / lifecycle id | ActorRef |
hi.audio().playCue / crossfade return | VoiceHandle |
Procedural setMesh(vertices) | no asset (CPU verts only) |
Dead / invalid ActorRef → host no-ops (never crash). Bad AssetRef / ContentRef → validate fail / soft no-op.
Implementation: sdk/src/asset_ref.zig, content_ref.zig, scene_ref.zig, prefab_ref.zig, actor_ref.zig. Kind + suffix table: sdk/src/asset_kind.zig (engine assets/asset_kind.zig re-exports). Host: host_api.zig + game_api/host_bind.zig.
Zig
const tex = hi.AssetRef.must(.texture, "asset://./textures/pbr_capsule_albedo");
const mat = hi.AssetRef.must(.material, "asset://./materials/default");
const cfg = hi.ContentRef.must("content://resources/ref_demo.json");
var parsed = try hi.json.parseRef(Config, allocator, cfg);
defer parsed.deinit();
const target = hi.world().find("target_zig");
if (!hi.world().isAlive(target)) return;
try hi.render().applyRender(target, .{
.mesh = hi.AssetRef.must(.model, "asset://./models/cube"), // optional
.material = mat,
.albedo = tex,
.notify_dirty = true,
.release_primitive = true,
});
hi.world().send(target, ping);
hi.world().send(hi.ActorRef.invalid, ping); // no-op
hi.world().requestSceneLoad(hi.SceneRef.must("scenes/refs_demo"), .{});- Literals:
AssetRef.must(kind, uri)/ContentRef.must/SceneRef.must(comptime non-empty; scene/asset cooked suffixes are compile errors). - Dynamic strings:
tryFromthen host validate. EntityIdis an alias ofActorRef.
Kawa
World.find(id) / Actors.find(id) → ActorRef | nil
Actor.is_alive / Actors.is_alive / send → safe on dead
Actors.has / get_number / set_number → component fields by name (dead → false/nil)
Content.read("content://…") → string | nil (content_dirs + AssetStore; no FS fallback)
Render.set_material(actor, mat [, alb])→ asset:// stems only (no .shin*)
Scene.set_skybox_texture("asset://…") → asset:// stem (primary)
Scene.set_skybox_texture_secondary(…) → asset:// stem (secondary)
Scene.set_skybox_blend(t) → dual cubemap mix [0,1]
Scene.load("scenes/foo") → SceneRef stem (not content://; not .json)ActorRef is weak (holding a ref never keeps an actor alive). Prefer Actors.* / hi.actors for new find/send/field access; full guide: Actor communication.
Demo
scenes/refs_demo (Browse scenes → Tutorials → References; on disk refs_demo.json / .shinscene):
| Actor | Role |
|---|---|
target_zig / target_kawa | ref_target — flash on ping |
driver_zig | ContentRef read → AssetRef material+albedo → ActorRef message |
driver_kawa | scripts/ref_demo — same |
Data: resources/ref_demo.json, resources/ref_demo_note.txt.