# Data-driven content, JSON, and paths

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](/docs/systems/refs.md).

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

| Root | Meaning | Typical location |
|------|---------|------------------|
| **content** | Shipped / staged read data | Product `data/`; editor often project root (parent of `scenes/`) |
| **writable** | Player settings (`configs/`), saves, mod overrides | Frontend `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>`) |
| **project** | Source project tree | Editor / dev when a project is open; empty in packaged standalone |

### URI schemes (preferred)

| Scheme | Root | Use |
|--------|------|-----|
| `content://path/under/root` | content | **Shipped / staged data** (JSON tables) via `ContentRef` + AssetStore |
| `writable://path` | writable | Saves, user mods (`ContentRef`) |
| `project://path` | project | Editor/dev source tree only (`ContentRef`) |
| `config://name.json` | `<game folder>/configs` | Config 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)

```
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`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/project_layout.zig) — do not hardcode `.engine` children in engine Zig.

| Path | Role | `kaji … --clean` |
|------|------|------------------|
| `build/` | Batch Shinra cook output | **wipe** |
| `staged/` | Intermediate packs + assets-only seal preview | **wipe** |
| `cache/` | Live Shinra cook, shader objects, Zig game-module caches | **wipe** |
| `bin/`, `lib/` | Editor-loaded game module + copied dylibs | **wipe** |
| `hot-reload/` | Hot-reload dylib snapshots | **wipe** |
| `logs/` | Editor NDJSON log (`editor-latest.json`) | **wipe** |
| `traces/` | Profiler file traces | **wipe** |
| `mcp-audit.jsonl` | Editor MCP audit log | **wipe** |
| `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.

| Stage | Owner |
|-------|--------|
| 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 prefabs | `assets` step, action `seal` — `HikariAssetConductor.StageRuntimeData` |
| Domain packs / loose seal | the 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:

| Form | Stage + package | With `--resources=bundles` |
|------|-----------------|----------------------------|
| `{ "path": "resources", "bundled": true }` | yes | may go into the `content` pack (`configs/layout.json`) |
| `"modding"` or `{ "path": "modding", "bundled": false }` | yes | **always 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](/docs/guides/localization.md). 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

| Environment | content root | project root | writable |
|-------------|--------------|--------------|----------|
| Editor (project open) | Derived from scene path (usually project root) | Project assets root | The product folder (shared with a packaged build of the project) |
| Standalone game | `…/data` next to the executable | Optional (dev `data_path`) | Frontend `writable_path`, else the product folder |
| Packaged install | App `data/` | empty | OS 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:

| Piece | Path |
|-------|------|
| Loose table | `hikari-games/example/resources/spawn_table.json` |
| Entity | `hikari-games/example/src/entities/json_spawner_entity.zig` (`json_spawner`) |
| Scene | `hikari-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.
