# API кеша игры (agent C1)

Клиент (iOS и Android, общий код `native/src/core/GameCache*.cpp`) при каждом запуске
сверяет папку игры с манифестом и **докачивает только те файлы**, у которых не совпадает
размер или хеш. Весь кеш заново не качается никогда.

| Файл | Что это |
|---|---|
| `generate_manifest.py` | генератор / проверщик манифеста (Python 3.11, без сторонних пакетов) |
| `cache_manifest.json` | манифест, сгенерированный из `C:\cachevarf` (1723 файла, 4.29 ГБ, скачивание 2.30 ГБ; 17 файлов, которые приложение везёт само в `gamefiles/`, исключены) |
| `cache_manifest.sha256` | SHA-256 самого манифеста (для контроля при заливке) |
| `manifest.php` | отдача манифеста с VDS (`http://94.228.167.74/api/cache/manifest.php`) |
| `tests/gamecache_test.cpp` | тест C++-части клиента на ПК (JSON, SHA-256/CRC-32, индекс, zip) |

Копия манифеста также лежит в `gamefiles/cache_manifest.bundled.json` — она попадает внутрь
приложения и используется, только если ни один сервер манифеста не ответил и кеш ещё ни
разу не синхронизировался.

## Раскладка на S3 (проверено)

`https://s3mobile.firsts3.ru/gamecache/zip/<путь>.zip` — **каждый файл лежит в своём zip**
(как в лаунчере Лит Мобайл, `MultiFileDownloader.kt`), внутри одна запись с именем файла,
deflate. Пример: `gamecache/zip/data/timecyc.dat.zip` (5917 байт) → `timecyc.dat` (51432 байта).
Несуществующий объект S3 отдаёт **403**, листинг бакета закрыт, `Range` поддерживается.

При генерации с `--probe-remote` скрипт читает центральный каталог **каждого** zip на S3
(Range-запросом, несколько КБ на файл) и сверяет CRC-32 и размер с локальным файлом.
Результат на 23.09.2026: все 1740 объектов на месте; отличается только `gtasa.ini`
(на S3 3561 байт, в `C:\cachevarf` 3411) — в манифест записана версия с S3 (`--remote-wins`),
и файл помечен `mutable` (конфиг, который пишет игра).

## Формат манифеста (`format: 1`)

```json
{
  "format": 1,
  "name": "santa-mobile-gamecache",
  "version": 202609231523,            // целое, растёт с каждой генерацией (UTC yyyymmddHHMM)
  "generatedAt": "2026-09-23T15:23:40Z",
  "baseUrl": "https://s3mobile.firsts3.ru/gamecache/zip",
  "layout": "filezip",                // filezip | files | zips
  "totalSize": 4618098599,            // сумма размеров файлов
  "allFilesSize": 4618098599,         // то же, имя из GameFileInfo Лит Мобайл
  "totalDownloadSize": 2472801193,    // сколько байт уйдёт по сети при полной закачке
  "fileCount": 1740,
  "files": [
    {"path": "data/timecyc.dat", "size": 51432,
     "sha256": "d507bb52…", "crc32": "fc9eba88", "zipSize": 5917},
    {"path": "gtasa.ini", "size": 3561, "sha256": "…", "crc32": "94d10f00",
     "zipSize": 1712, "mutable": true}
  ]
}
```

Поля записи:

- `path` — путь от корня папки игры, через `/`, с тем регистром, как в кеше.
- `size` — точный размер в байтах; `sha256` — hex в нижнем регистре; `crc32` — 8 hex-цифр.
- `zipSize` — размер `<path>.zip` на S3 (раскладка `filezip`); `zipEntry` — имя записи в zip,
  если оно не совпадает с именем файла.
- `url` — необязательный абсолютный адрес. Без `zipSize` это адрес самого файла, с `zipSize` —
  адрес его zip.
- `archive` — файл лежит внутри большого zip (раскладка `zips`):
  `{url, entryName, compressedSize, method (0/8), localHeaderOffset, dataOffset}`. Клиент
  одним `Range`-запросом забирает ровно байты этой записи и распаковывает их.
- `mutable: true` — конфиги, которые пишет игра/игрок (`gta_sa.set`, `SAMP/settings.ini`,
  `gtasa.ini`, `reVC.ini`, …): качаются только если файла **нет**, при несовпадении хеша не
  трогаются.

Откуда клиент качает файл: `archive` → Range из большого zip; `layout: filezip` (или есть
`zipSize`) → `url` или `baseUrl + "/" + path + ".zip"`; иначе (`files`) → `url` или
`baseUrl + "/" + path`.

## Как клиент проверяет кеш

1. Берёт манифест по адресам (по порядку): dev-override → `https://s3mobile.firsts3.ru/gamecache/manifest.json`
   → `http://94.228.167.74/api/cache/manifest.php` → последний применённый
   (`.cache_manifest.local.json` в папке игры) → встроенный в приложение. Если манифеста нет
   вообще, но кеш в папке есть — игра запускается без проверки (офлайн).
2. **Быстрый проход**: `stat()` каждого файла. Нет файла → качать. Размер не тот → качать.
   Размер и mtime совпадают с записью в `.cache_index.json`, а её SHA-256 совпадает с
   манифестом → файл хороший, не читается. Здоровый кеш проверяется за доли секунды.
3. **Глубокий проход** при запуске — только для файлов, за которые индекс не ручается (первый
   запуск на уже скопированном кеше, файлы, изменённые с прошлого раза, файлы, у которых в новой
   версии манифеста другой хеш): CRC-32 и SHA-256 за одно чтение, 4 потока.
4. **Закачка** только плохих/отсутствующих файлов: 3 потока, докачка `Range` после паузы,
   обрыва сети или перезапуска приложения, 6 попыток с паузами 1–16 с, проверка свободного
   места заранее. Каждый файл пишется в `<файл>.part`, проверяется размер + CRC-32 + SHA-256,
   и только потом атомарно (`rename`) заменяет старый. Битая или недокачанная загрузка никогда
   не портит хороший файл.
5. Файлы, которых нет в манифесте, не удаляются (только пишутся в `gamelog.txt`).
6. Пока идёт проверка/закачка, игра ждёт (`GameCache_RunStartupGate` в `sdl2.cpp`). Экран
   `cache_download_screen.dart` показывается **только когда есть реальная работа** (закачка,
   первая проверка больше 128 МБ, ошибка) — поле `ui` в статусе. Здоровый кеш проверяется за
   загрузочным экраном, без мигания.
7. **Всё автоматически, кнопки нет.** Полный проход SHA-256 по всем файлам запускается сам в
   фоне через 90 с после старта игры, с низким приоритетом (1 поток, ~30 МБ/с, background QoS /
   nice 10), если: версия манифеста отличается от той, по которой был последний полный проход,
   или последний полный проход был больше 7 дней назад. Найденные битые файлы сразу
   перекачиваются в фоне (атомарная замена; игра дочитывает уже открытый старый файл, новый
   используется со следующего запуска). Если фоновая закачка не удалась — запись файла
   удаляется из индекса, и следующий запуск чинит его уже с экраном.
8. Новая версия манифеста на сервере применяется при следующем запуске сама: скачиваются
   ровно те файлы, у которых изменились размер или хеш.

## Как перегенерировать

```bat
cd C:\sourceIOS\IOSSOURCE\api
python generate_manifest.py C:\cachevarf --probe-remote --remote-wins
python generate_manifest.py --verify C:\cachevarf        :: должно быть OK
```

- `--probe-remote` сверяет каждый файл с zip на S3; при расхождениях без `--remote-wins`
  скрипт завершится с кодом 1 и перечислит их. `--remote-wins` описывает такие файлы так, как
  они лежат на S3 (клиент ведь качает с S3).
- `--check-remote` — перепроверить уже готовый манифест против S3 (после перезаливки файлов).
- `--exclude <glob>` / `--mutable <glob>` — дополнительные исключения / «конфиги».
- `--version N` — задать версию вручную (по умолчанию UTC-время генерации).
- `--layout files --base-url <url>` — если на S3 файлы лягут как есть, без zip.
- `--layout zips --zip-map map.json` — если файлы будут упакованы в большие zip:

  ```json
  {"zips": [
    {"url": "data.zip", "prefix": "data/", "local": "D:/upload/data.zip"},
    {"url": "https://s3mobile.firsts3.ru/gamecache/zip/texdb.zip", "prefix": "texdb/"}
  ]}
  ```
  `url` — абсолютный или относительно `--base-url`; `prefix` — что дописать к имени записи в zip,
  чтобы получился путь в кеше; `local` — локальная копия того же zip (иначе каталог читается с S3
  Range-запросами). Смещения записей заполняются автоматически.

Генератор заодно копирует манифест в `gamefiles/cache_manifest.bundled.json` (отключить:
`--no-bundle`).

Файлы, которые приложение везёт само (`gamefiles/`: свои GXT, `fonts_r.txd`, `generic.txd`,
`gamecontrollerdb.txt`, …), в манифест **не попадают** — иначе клиент «чинил» бы эти
подменённые файлы обратно на версию из кеша. Сейчас это 17 файлов. Другая папка:
`--bundled-dir <dir>`, отключить: `--no-exclude-bundled`.

## Куда заливать

1. **S3**: `cache_manifest.json` → `gamecache/manifest.json` (публичное чтение, `Content-Type:
   application/json`). Это основной адрес клиента. **Сейчас этого объекта нет (403)** — пока он
   не залит, клиент берёт манифест с VDS или встроенную копию.
2. **VDS** (резерв): `manifest.php` + `cache_manifest.json` (+ `.sha256`) в
   `/api/cache/` → `http://94.228.167.74/api/cache/manifest.php`.
   - `?since=<version>` → `{"changed":false,"version":N}`, если версия та же;
   - `?file=<path>` → одна запись; `?info=1` → только заголовок;
   - `ETag` / `Last-Modified` / `304`, gzip.
3. Сами файлы — как сейчас: `gamecache/zip/<путь>.zip`.

Порядок обновления кеша: залить новые/изменённые zip → сгенерировать манифест с
`--probe-remote` → залить манифест **последним**. Тогда клиент никогда не увидит манифест,
описывающий файлы, которых ещё нет на S3. Если заменить zip на S3, не обновив манифест,
клиент скачает файл, увидит несовпадение SHA-256 и покажет ошибку (а старый файл не тронет).

## Версии и инвалидация

- `version` — целое число; клиент сохраняет применённый манифест в `.cache_manifest.local.json`.
- Изменили один файл → новая генерация → у клиента не совпадёт размер/хеш ровно у этого файла →
  скачается только он. Удалили файл из кеша → он просто перестанет проверяться (с устройства не
  удаляется). Добавили → докачается.
- Индекс `.cache_index.json` (путь → размер, mtime, SHA-256, CRC-32) — только ускорение;
  его можно удалить, тогда следующий запуск один раз пересчитает хеши.
- Dev-override адреса манифеста: ключ настроек `cache_manifest_url` (iOS `NSUserDefaults`
  `revc.stage.setting.cache_manifest_url`, Android SharedPreferences `revc.stage.settings`)
  или текстовый файл `.cache_manifest_url` в папке игры с одним URL.
