# `$.reload` — горячая перезагрузка ассетов (§27)

Перезагрузка во время работы: правка текстуры, раскладки атласа, данных коллизии
карты или документа/стиля RmlUi применяется к живой игре — либо согласованно,
либо отклоняется без повреждения (last-known-good). Три операции различаются
явно:

| Что | Чем | Что происходит |
|---|---|---|
| **Reload данных** | `$.reload.reload(name)`, `poll()`, `reloadAll()` | новый ассет читается, проверяется и только потом подменяет прежний |
| **Reload поведения** | `$.reload.behavior(reason)` → перезапуск скриптов движком | сохранение произвольных JS-замыканий **не обещается** |
| **Restart сцены** | `$.reload.restartScene(name, opts)` | сцена собирается заново (`$.scene.load(..., { scope: true })`), поддерживаемое логическое состояние переносится `capture()/restore()` |

## Основное

```js
$.reload.watch('hero', { kind: 'atlas', path: 'assets/hero.json', atlas: 'hero',
                         deps: ['hero_page'] });
$.reload.reload('hero');            // { ok, stage, code, error, version, changed, deps, warnings }
$.reload.reloadAll();               // весь граф в порядке зависимостей
$.reload.poll();                    // применить только изменившееся на диске (кнопка F8)

$.reload.list();                    // записи реестра
$.reload.get('hero');               // { name, kind, version, status/state, path, deps, last, hash }
$.reload.value('hero');             // текущее ЗНАЧЕНИЕ (то, что видит игра)
$.reload.version('hero');           // номер версии; 0 — ещё не применялось
$.reload.versions('hero');          // история: { version, hash, frame, path }
$.reload.graph();                   // { nodes, edges, cycles }
$.reload.order(['ui', 'hero']);     // топологический порядок имён
$.reload.pending();                 // handle'ы, ждущие освобождения
$.reload.flush(frame);              // освободить то, чей кадр уже прошёл
$.reload.stats();                   // { watched, applied, failed/failures, retired, live, … }
$.reload.indicator();               // компактное состояние для панели
$.reload.on('applied' | 'failed', fn);   // псевдонимы: 'reload' | 'fail'
```

## Конвейер: prepare → apply

| Стадия | Что делает | Публикует? |
|---|---|---|
| `read` | читает источник (`$.fs`, `engine.readBytes`) | нет |
| `validate` | битый JSON, отсутствующая страница атласа, разметка RmlUi, схема | нет |
| `prepare` | готовит новое значение во временной области | нет |
| `apply` | подменяет прежнюю версию, перепривязывает спрайты | да |

Ошибка любой стадии → `{ ok: false, stage, code, error }`, структурная запись в
`$.debug.reportError` (§17), значение и версия **не меняются**: игра работает на
прежней версии.

Коды: `R2D_RELOAD_READ`, `R2D_RELOAD_PARSE`, `R2D_RELOAD_VALIDATE`,
`R2D_RELOAD_PREPARE`, `R2D_RELOAD_APPLY`, `R2D_RELOAD_MISSING_DEP`,
`R2D_RELOAD_ADAPTER`, `R2D_RELOAD_CYCLE`, `R2D_RELOAD_FRAME_MISSING`,
`R2D_RELOAD_TEXTURE_SIZE`, `R2D_RELOAD_RML`, `R2D_RELOAD_STYLE_CACHED`,
`R2D_RELOAD_UNSUPPORTED`, `R2D_RELOAD_FAILED`.

```js
const r = $.reload.reload('hero');
if (!r.ok) $.debug.reportError('reload:hero', r.error, { code: r.code });
```

## Граф зависимостей

`watch(name, { deps: [...] })` описывает, что от чего зависит: `atlas → pages →
clips/sockets`, `tilemap → collision/occlusion`, `RmlUi → styles/fonts/images`.
`reloadAll()` обходит граф в порядке `order()`, поэтому страница атласа
подменяется раньше кадров, которые на неё ссылаются; `reload(name)` с
`{ deps: true }` (по умолчанию) делает то же для одной ветки. Цикл — явная
ошибка `R2D_RELOAD_CYCLE`, а не зависание.

```js
$.reload.watch('hero_page', { kind: 'texture', path: 'assets/hero.png',
                              texture: () => $.atlas.get('hero').texture });
$.reload.watch('hero', { kind: 'atlas', path: 'assets/hero.json', atlas: 'hero',
                         deps: ['hero_page'] });
```

## Last-known-good

Неудачная перезагрузка **не ломает игру**: прежняя версия остаётся рабочей, а
результат приходит структурой с кодом. Битый JSON, отсутствующая страница
атласа, пропавший используемый кадр и плохая разметка дают `ok: false`;
игра продолжает отвечать и рисовать прежнюю версию.

## Версии и идентичность ресурсов

Игра ссылается на ресурс по **имени и версии**, а не по native-handle.
Версия растёт только когда данные действительно изменились — сравнение по
подписи содержимого (FNV-1a), а не по mtime (mtime врёт при checkout и
копировании). Для `$.resource` версия растёт вместе с заменой
(`$.resource.publish`), см. [resource.md](highlevel/resource).

## Освобождение handle'ов

Старые handle'ы не освобождаются в кадре замены: `apply` возвращает
`retire: [{ kind, handle, name }]`, а кадровый ход модуля освобождает их на
**следующем** кадре (`$.reload.flush`, `$.reload.pending()`). Текстура
освобождается `engine.freeTexture`, документ RmlUi — `engine.ui.unload`, ресурс —
`$.resource.remove`.

`remapNodeSprites(nodes, oldToNew)` переносит ссылки узлов на новые id кадров,
`usedSpriteNames(sheet)` показывает, какие кадры игра реально использует (по
этому списку видно, какие кадры нельзя выкинуть при обновлении листа).

## Встроенные виды

### `texture`

`{ kind: 'texture', path, texture | resource }`. Перезагрузка **на месте**:
`engine.reloadTexture` читает картинку и заливает её в ту же текстуру — handle не
меняется, узлы и спрайты продолжают работать, новых handle'ов не появляется.
Смена **размера** отклоняется (`R2D_RELOAD_TEXTURE_SIZE`): спрайты хранят
uv-прямоугольник, нормированный по старому размеру.

### `atlas`

`{ kind: 'atlas', path, atlas?, load?, deps? }`, где `atlas` — имя в `$.atlas`
(по умолчанию имя ассета).

* та же раскладка, другое содержимое — картинка обновляется на месте, кадры не
  пересобираются: **ни одного нового спрайта**;
* изменённая раскладка — кадры пересобираются, спрайты узлов перепривязываются
  **по имени кадра**, старая ссылка на объект атласа продолжает работать
  (методы делегируются новой версии); результат отдаёт
  `frame_map: { имя: { old, new } }`;
* используемый в мире кадр исчез из новой раскладки — отказ
  `R2D_RELOAD_FRAME_MISSING` (устаревший frame ID не привязывается молча);
* пересборка не удалась — атлас восстанавливается на прежних данных.

### `tilemap`

`{ kind: 'tilemap', path, target? }` (`target` по умолчанию `#map`). Проверяются
`layers[].data` (или `data`), длины массивов и допустимость id. Применение — через
публичные `.setTile()`/`.rebuild()`: карта сама пересобирает визуал, коллизию и
occluders одним проходом, поэтому между кадрами нет «старой невидимой стены» или
«новой видимой платформы без коллизии». Неизменившиеся клетки не трогаются.

### `rml`

`{ kind: 'rml', path }`. Проверка — сбалансированность тегов (RmlUi разбирает XML
терпимо и незакрытый тег примет) плюс пробная загрузка во временный документ.
Применение — `doc.unload()` и `$.ui.doc(path)` заново; `value` результата — новый
handle документа (игре нужно взять его оттуда).

Ограничение: RmlUi кэширует **внешние** `.rcss` по URL. Правка связанного стиля
даёт предупреждение `R2D_RELOAD_STYLE_CACHED` (и запись в `$.debug.errors()`), а
не применённый стиль. Стиль, встроенный в сам `.rml` (`<style>`/`style=`),
применяется сразу.

### `json`

`{ kind: 'json', path, validate?, apply? }`. Значение читается через
`$.reload.value(name)`; `validate(data)` возвращает `true`, строку-причину или
`{ ok, error }`. Битый JSON — `R2D_RELOAD_PARSE`, игра продолжает работать.

## Свои виды ассетов

```js
$.reload.registerKind('my-format', {
    load(path, env) { /* вернуть значение */ },
    validate(value) { /* true | строка-причина */ },
    apply(value) { /* необязательно: применить */ },
});
$.reload.kinds();     // ['texture', 'atlas', 'tilemap', 'rml', 'json', 'my-format']
```

Полный адаптер конвейера (`read/validate/prepare/apply`) принимается как есть;
упрощённый (`load/validate/apply`) оборачивается в него. Встроенные адаптеры —
`builtinAdapters()`. Правок движка не требуется.

## Scoped restart

Если безопасный reload поведения невозможен, `$.reload.behavior(reason)`
запрашивает перезапуск JS-рантайма на границе кадра, а
`restartScene(name, { capture, restore })` загружает сцену с новой областью
владения §5 и применяет перенесённое состояние на следующем кадре:

```js
$.reload.restartScene('level1', {
    capture: () => ({ hp: 50, room: 3 }),      // только JSON-совместимое
    restore: (s) => { /* применить */ },
    transition: 'fade', ms: 300,
});
```

## Наблюдение и панель

Панель DevTools (F1) показывает число ассетов под наблюдением, неудачные,
последний результат и очередь освобождения; кнопки «Перезагрузить ассеты» и
«Применить изменившиеся» вызывают `reloadAll()`/`poll()`. Программно —
`$.devtools.assets()`, `$.devtools.assetsReload()`, `$.devtools.assetsPoll()`.

## Ограничения

* Сохранение произвольных JS-замыканий после правки кода не обещается: только
  scoped restart с переносом логического состояния.
* Смена **размера** текстуры — не хот-релоад: нужен новый спрайт-набор/сцена.
* **Внешний `.rcss`** RmlUi кэширует по URL — правка стиля даёт
  `R2D_RELOAD_STYLE_CACHED`.
* **Изменение раскладки атласа** создаёт новые спрайты: таблица спрайтов
  (`src/render.c`) не переиспользует слоты выгруженных. Неизменившаяся раскладка
  не создаёт ни одного спрайта — серия 100 reload даёт нулевой прирост.
* Перепривязка идёт по `node.sprite` и массивам `node.frames`; **клипы**,
  собравшие спрайты заранее, нужно пересобрать по `frame_map` из результата.
* `poll()` читает файлы, чтобы сравнить подпись: вызывайте по кнопке или
  таймеру, а не каждый кадр.
* Кэш SDK (`site/`, `build/sdk-*`) к игре отношения не имеет: итоговый пакет от
  него не зависит.

## Тесты

`tests/js/reload_test.mjs` (ядро без движка: стадии, откат, граф, циклы, версии,
отложенное освобождение) и `tests/agent/highlevel_reload_test.py` (фикстура
`tests/fixtures/reload`): замена текстуры/раскладки атласа/данных коллизии/
документа RmlUi, last-known-good при битом ассете, согласованность коллизии и
визуала карты, серия 100 reload без роста узлов/тел/подписок/ресурсов/текстур,
освобождение старого handle'а на следующем кадре.
