Skip to content
hikari
RenderingEditorAIToolchainDocumentation
Download

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsDownloadLicenseBack to top
Documentation / Guides
Browse docs
Overview
Tutorials17
OverviewPlay, Edit, and scenesFirst UI documentFirst game projectFirst entityComponent properties and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides25
OverviewWorking in the editorDevelopment guideGameplay APIActor and component lifecycleThreads, jobs, events, and routinesGameplay AI tasksGame AI: perception and movement playgroundBrain assetsBrain execution for swarm populationsLocalizationGame cursorsChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringChoosing and scaling shadowsTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / Unreal
Systems32
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorPipeline compilationSkeletal animationGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitUI documentsAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)Document schemasUI and editorEditor asset hot reloadEditor Project Selector
Language reference3
OverviewAkari GPU ABIAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials17
OverviewPlay, Edit, and scenesFirst UI documentFirst game projectFirst entityComponent properties and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides25
OverviewWorking in the editorDevelopment guideGameplay APIActor and component lifecycleThreads, jobs, events, and routinesGameplay AI tasksGame AI: perception and movement playgroundBrain assetsBrain execution for swarm populationsLocalizationGame cursorsChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringChoosing and scaling shadowsTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / Unreal
Systems32
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorPipeline compilationSkeletal animationGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitUI documentsAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)Document schemasUI and editorEditor asset hot reloadEditor Project Selector
Language reference3
OverviewAkari GPU ABIAkari language referenceKawa language reference
Engine overview
Guides8 min read

Data-driven content, JSON, and paths

On this page
On this pagePath rootsURI schemes (preferred)JSONProject layout (loose content)Project .engine/ (staging)Staging (Kaji, not Zig)Moddable packsEnvironmentsSample: JSON spawnerWhat this is not Back to top

Game modules get two complementary tools:

  1. hi.json — fast type-driven JSON (no host vtable on parse/stringify).
  2. hi.ContentRef + hi.paths — sandboxed content identity and IO (same in editor, debug product, and packaged builds).

Cooked runtime assets use hi.AssetRef (asset://…) — not content URIs. Full identity map: Game-facing refs.

Shinra still owns cooked assets under assets/. Loose data tables (JSON, configs, mod hooks) live beside the project and stage into data/ without cooking.

Path roots

RootMeaningTypical location
contentShipped / staged read dataProduct data/; editor often project root (parent of scenes/)
writablePlayer settings (configs/), saves, mod overridesFrontend writable_path when the platform owns storage (console save data, store sandbox); else the product folder (macOS ~/Library/Application Support/<identifier>, Windows %LOCALAPPDATA%\<publisher>\<title>, Linux $XDG_DATA_HOME/<identifier>)
projectSource project treeEditor / dev when a project is open; empty in packaged standalone

URI schemes (preferred)

SchemeRootUse
content://path/under/rootcontentShipped / staged data (JSON tables) via ContentRef + AssetStore
writable://pathwritableSaves, user mods (ContentRef)
project://pathprojectEditor/dev source tree only (ContentRef)
config://name.json<game folder>/configsConfig facade (Config.load / edit) and Kawa Content.read — not a ContentRef scheme

Relative path after the scheme is sandboxed (no .., no absolute paths). Same idea as asset:// for cooked assets.

zig
const hi = @import("hikari_game");

// Preferred: ContentRef
const table = hi.ContentRef.must("content://resources/spawn_table.json");
const bytes = try hi.paths.read(allocator, table);
defer allocator.free(bytes);

// Absolute OS path (caller frees)
const path = try hi.paths.resolve(allocator, table);
defer allocator.free(path);

// Saves
try hi.paths.write(hi.ContentRef.must("writable://saves/slot0.json"), json_bytes);

Root-enum helpers (join / readFile with .content) exist on hi.paths; game code should prefer ContentRef.

Read a configured root with hi.paths.rootPath(.content); it returns null when unavailable.

JSON

Zig std.json is the only JSON parser and typed stringify in product Zig. Game code goes through hi.json (this page). Engine code uses the same std.json plus helpers in src/hikari/src/utils/json.zig (scan / write / authoring number hygiene) — do not add another tokenizer or DOM.

Type-driven parse (compile-time shape). Defaults: ignore unknown fields (mod-friendly), alloc_always (strings outlive the source buffer so parseUri / parseFile can free file bytes immediately), compact stringify.

zig
const SpawnTable = struct {
    version: u32 = 1,
    spawns: []const struct {
        id: []const u8,
        archetype: []const u8,
        position: [3]f32 = .{ 0, 1, 0 },
    },
};

// From memory
var parsed = try hi.json.parse(SpawnTable, allocator, bytes);
defer parsed.deinit();

// From content (preferred)
var from_disk = try hi.json.parseRef(SpawnTable, allocator, hi.ContentRef.must("content://resources/spawn_table.json"));
defer from_disk.deinit();

// Scratch / arena (no Parsed wrapper)
const table = try hi.json.parseLeaky(SpawnTable, arena.allocator(), bytes);

// Write compact JSON under writable
try hi.json.writeUriAlloc(allocator, "writable://saves/progress.json", save_data);

Hot path tips:

  • Prefer concrete structs over std.json.Value.
  • Use parseLeaky + a frame/arena allocator for one-shot tables.
  • Prefer stringify (compact) over pretty for saves.
  • Keep tables small enough to load once at session start when possible.

Project layout (loose content)

text
my_game/
├── hikari.project.json
├── configs/                 # solo configs → product data/configs/
│   ├── game.json
│   ├── render.json
│   ├── drivers.json
│   ├── editor.json
│   ├── packaging.json
│   ├── layout.json          # pack graph: membership, delivery, deps, labels
│   ├── input_actions.json
│   └── gameplay_tags.json   # tag catalog (not session compose)
├── assets/                 # the one authoring tree: scenes, models, materials, scripts … anywhere under it
├── resources/              # content dir tables (content pack when bundled:true + resources=bundles)
│   └── example_items.json
└── .engine/                # build/cache/stage only (never author here)

The project tree is source only. Product data/ lives under bin/game/data/ (or packaged install); assets-only tooling may seal a preview under .engine/staged/data/. Do not put a data/ stage mirror next to authoring folders.

Project .engine/ (staging)

All generated engine state for a project lives under <project>/.engine/. Gitignore the whole tree (sample does). Zig SSOT for these segments is src/hikari/src/project_layout.zig — do not hardcode .engine children in engine Zig.

PathRolekaji … --clean
build/Batch Shinra cook outputwipe
staged/Intermediate packs + assets-only seal previewwipe
cache/Live Shinra cook, shader objects, Zig game-module cacheswipe
bin/, lib/Editor-loaded game module + copied dylibswipe
hot-reload/Hot-reload dylib snapshotswipe
logs/Editor NDJSON log (editor-latest.json)wipe
traces/Profiler file traceswipe
mcp-audit.jsonlEditor MCP audit logwipe
user/Editor chrome prefs (prefs.json) + MCP enable/host/port (mcp.json; token in OS secure storage)keep

--clean also clears workspace bin/* (except kaji) and build/cache/* (except kaji). It never deletes project source (assets, scenes, configs, resources).

Staging (Kaji, not Zig)

Kaji owns all product data staging and seals. Zig build.zig is compile/link only (binaries, libraries) — it never copies scenes, configs, scripts, materials, or packs.

StageOwner
Cook (Shinra; scripts, materials, art, scenes, prefabs)assets step, action cook — writes only <project>/.engine/build/shinra
configs/, hikari.project.json, content_dirs, cooked scenes and prefabsassets step, action seal — HikariAssetConductor.StageRuntimeData
Domain packs / loose sealthe same seal action — ProductResourcePacker.SealAsync (configs/layout.json)

Each data root has exactly one writer, and that writer rebuilds it from sources every time it runs, so a reseal never depends on what an earlier seal left behind. Destinations: product bin/game/data/ (the game seal, or the editor's editor-data step) or the kaji assets preview .engine/staged/data/. Never into project authoring paths.

On kaji game … / kaji assets …, after the cook:

  1. Empty the data root, then stage configs/, the descriptor, every content_dirs entry (see rules below), and the cooked scenes and prefabs.
  2. Seal: pack, catalog, ship-exclude.

All listed content_dirs stage and ship under product data/<name>/. The bundled flag is pack vs naked files, not ship vs omit:

FormStage + packageWith --resources=bundles
{ "path": "resources", "bundled": true }yesmay go into the content pack (configs/layout.json)
"modding" or { "path": "modding", "bundled": false }yesalways loose under data/modding/ (easy player/mod edit)
  • Omitted field → stage resources/ (loose-mod) if it exists.
  • "content_dirs": [] → stage nothing.
  • Single-segment path only; invalid/duplicate entries warn and skip.

Moddable packs

content_dirs cover data tables. Cooked assets are moddable through a pack whose delivery is loose:

json
{ "stem": "mods", "role": "files", "delivery": "loose", "root": "mods" }

The pack ships as the directory data/mods/, catalogued but not bundled — and it is catalogued even when empty, so a game can open it before anything exists to put in it. While the pack is retained, a cooked file dropped into that directory after the build resolves exactly like one the seal placed. Outside a loose pack's root, an uncatalogued cooked path resolves nowhere.

Three consequences worth knowing:

  • Mods ship cooked, not source. A loose pack holds .shinmodel / .shintexture, so modders need the Shinra cook tool, not just the engine.
  • Mod scenes need no build step. A scene document declares no pack membership; the runtime derives it from the assets the scene references. A .shinscene authored against a shipped product loads like one that shipped with it.
  • Loose is additive, not an override. A mod cannot replace a path that already belongs to a packed pack — one logical path has exactly one owner. Shadowing (delivery: "overlay") is reserved and not implemented.

Opening a mod pack from game code:

zig
const packs = hi.packs() orelse return;
const handle = packs.open("mods");
if (handle == .none) return;
// Store the handle on the owner; poll packs.state(handle) on later callbacks.
// Once .ready, acquire assets. Close the handle when the owner is finished.

open returns immediately. Cold storage opens on a worker; state(handle) distinguishes pending, ready, failed and closed. Closing a pending handle cancels it without blocking. packs also has openLabel for opening a themed group at once, and stat(stem) which reports whether a pack is moddable — that is, loose. All of this works in Play-in-editor too: the editor's store reads the cook plan as its catalog, so open("mods") succeeds and stat counts the retain, without a build.

Shinra never cooks these dirs — only assets/ is fed to Shinra. Put cookable art under assets/; put JSON tables, balance sheets, and mod hooks under content dirs. Compiled translations belong in assets/localization/; see localization. Prefer bundled: false for files users should edit without unpacking a pack; bundled: true for ship tables you are fine packing.

Runtime allowlist + store: content:// first path segment must be in content_dirs. Reads go only through AssetStore (product data_root / packs — same loose vs bundled config as assets). No raw filesystem fallback for content://. writable:// / project:// / config:// remain FS (via fs.system()). Scenes use SceneRef, not ContentRef.

config:// (engine config.zig): one choke point for config IO under a specific root's configs/. Generic load / edit / write; typed shortcuts (sessionConfig, input) only convert domain types. Runtime hosts compose authored/staged defaults with a writable per-user layer; games access that resolution through hi.config, whose writes never mutate the authored install. Add future domains through the shared filename/kind/version registry, not ad-hoc paths. Kawa Content.read("config://…") remains the session's authored-root filesystem view.

If you remove a content dir from the project file, an old copy may still sit under bin/game/data/ until the next clean stage of that tree (or --clean).

Environments

Environmentcontent rootproject rootwritable
Editor (project open)Derived from scene path (usually project root)Project assets rootThe product folder (shared with a packaged build of the project)
Standalone game…/data next to the executableOptional (dev data_path)Frontend writable_path, else the product folder
Packaged installApp data/emptyOS persistent

Mods can:

  1. Drop override files under the writable tree with the same relative path and use hi.paths.readFileSearch, or
  2. Replace / add files next to the install under data/resources/ (unpacked loose layout).

Sample: JSON spawner

The sample project exercises the full loop:

PiecePath
Loose tablehikari-games/example/resources/spawn_table.json
Entityhikari-games/example/src/entities/json_spawner_entity.zig (json_spawner)
Scenehikari-games/example/scenes/runtime_spawn_showcase.json

On Play, the spawner calls hi.json.parseRef(SpawnTable, …, ContentRef) (or parseUri with the same URI) and hi.world().spawn for each entry (layer inherited from the spawner). Cubes fall with physics at the back of the combined spawning scene; the left and right stations demonstrate direct Zig and Kawa spawning.

sh
bin/kaji/kaji game --workspace="$PWD" --project=../hikari-games/example --type=dynamic --config=debug --run
# or editor: load scenes/runtime_spawn_showcase.json and Play

Expect logs like json_spawner: table v1, 3 entries and json_spawner: spawned json_cube_a ….

What this is not

  • Not a second asset pipeline (no asset:// for these files).
  • Not encrypted or signed content (ship/sign at packaging level if needed).
  • Not a full VFS over .shinbundle — use AssetStore for cooked assets.
PreviousHikari Plugin APINext Scripting with Kawa

Documentation follows the current engine checkout.

Snapshot 86f12d68Source docs/guides/data-and-modding.md
On this pagePath rootsURI schemes (preferred)JSONProject layout (loose content)Project .engine/ (staging)Staging (Kaji, not Zig)Moddable packsEnvironmentsSample: JSON spawnerWhat this is not Back to top