Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Guides
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
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 governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
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 governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari 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/)
writableUser saves + mod overridesOS persistent: …/Hikari/<product_id>/
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.

Facade for the host table: hi.paths_api().root(.content).

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)
├── scenes/
├── assets/                 # Shinra-cooked → `assets` packs from configs/layout.json
├── 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
plugin-settings/Package settings (hi.settings → <package_id>.json)wipe
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
Authoring trees (scenes/, assets/scripts → scripts/, assets/materials → materials/, configs/, hikari.project.json)HikariAssetConductor.StageAuthoringRuntimeTrees (no project bundles/ — packs from Shinra/seal only)
Loose content_dirsHikariAssetConductor.StageLooseContentDirs
Cooked scenes / art packsShinra + StageCookedScenes / StageBundle
Domain packs / loose sealProductResourcePacker.SealAsync (configs/layout.json)

Combined entry: StageProjectRuntimeData (authoring + content dirs). Destinations: product bin/game/data/, engine install bin/hikari/bin/data/, or assets-only .engine/staged/data/. Never into project authoring paths. Units stage binaries with typed FS steps (KajiPlan.Copy), not host shell soup.

On kaji game … / kaji assets …, after the Shinra bundle:

  1. Stage authoring trees + read content_dirs from hikari.project.json (see rules below).
  2. Replace each project content <path>/ → product data/<path>/ (bin/game/data/, bin/hikari/bin/data/; assets-only → .engine/staged/data/). Destinations are wiped before copy so deletes in the project tree do not leave stale files.

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.host_api.packs() orelse return;
const handle = packs.open("mods");   // resolvable, not yet loaded
defer _ = packs.close(handle);

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, localization, balance sheets, and mod hooks under content dirs. 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 rootOS persistent Hikari/editor
Standalone game…/data next to the executableOptional (dev data_path)OS persistent Hikari/game
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 tablesrc/games/example/resources/spawn_table.json
Entitysrc/games/example/src/entities/json_spawner_entity.zig (json_spawner)
Scenesrc/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=src/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 cc148c75Source 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