# План: переход к клиент-серверной игре Статус: выполняется. Дата: 2026-10-30. Проверенный прогресс отмечен в разделе 9. ## 1. Цель и рамки Игра: симулятор бога в духе RimWorld, но с меньшим влиянием игрока. Город живёт сам (жители, машины, потребности, события), игроки в основном наблюдают и ограниченно вмешиваются. По запросу автора от 2026-10-30 этап 5 включает базовые команды вмешательства. Исходные требования: - До 30 игроков в одной комнате. - Комната — отдельный инстанс мира, у каждой комнаты свой город (из OSM). - Сущностей очень много, до миллиона на комнату. - Клиент остаётся в браузере и только отображает. Вся симуляция и правила на сервере. - Сгенерированный город хранится готовым, генерация не выполняется в рантайме комнаты. Не входит в первую версию: аккаунты и монетизация, предсказание на клиенте (в наблюдательной игре оно не нужно). Влияние ограничено командами этапа 5. ## 2. Ключевые решения | Вопрос | Решение | Почему | |---|---|---| | Язык сервера | Rust | Нет GC-пауз, плотные данные, параллельность без data race | | ECS | `bevy_ecs` как отдельная библиотека (без движка Bevy) | Архетипы, автопараллелизм систем, change detection, `Schedule`. Запасной вариант: `hecs` | | Асинхронность и сеть | `tokio` + `axum` + `tokio-tungstenite` | Стандарт для WebSocket в Rust | | Транспорт | WebSocket (бинарный) | В браузере работает везде, TCP достаточен: игроки наблюдают, потеря пакета не критична, мир обновляется ~5–10 раз в секунду. WebTransport подключим, только если упрёмся в задержки | | Формат сообщений | Protobuf (`prost` на сервере, `ts-proto` или `protobuf-es` на клиенте) | Общая схема, кодогенерация для обеих сторон, компактно, версионируется | | Статика города | Тайлы-файлы, отдаются по HTTP | Кэшируются браузером и CDN, не грузят сервер симуляции | | Генерация города | Остаётся на TypeScript, запускается офлайн (CLI) | Уже написана и отлажена (около 5 тыс. строк), портировать нет смысла | | Сохранения | Снимки мира на диск, позже БД | Комнат немного, симуляция детерминируемая | ## 3. Архитектура ``` Браузер (TS) Сервер (Rust) ┌───────────────┐ HTTP ┌───────────────────────┐ │ Рендерер │◄─────────►│ Статика: тайлы города │ │ (src/view) │ тайлы └───────────────────────┘ │ │ WS ┌───────────────────────┐ │ Состояние │◄─────────►│ Gateway (axum) │ │ клиента: │ дельты │ - подключение к комн.│ │ интерполяция │ └──────────┬────────────┘ └───────────────┘ │ каналы ┌───────────▼────────────┐ │ Room (1 поток/задача) │ │ bevy_ecs World │ │ Schedule систем │ │ Interest manager │ └────────────────────────┘ ``` Принципы: 1. **Одна комната — один `World`.** Комнаты независимы, масштабируются добавлением потоков и процессов. Внутри комнаты системы выполняются параллельно средствами `bevy_ecs`. 2. **Сервер авторитарен.** Клиент отправляет только запросы (подписка на область, позже команды влияния), состояние никогда не принимает от клиента. 3. **Статика отдельно от динамики.** Здания, дороги, интерьеры и мебель из OSM лежат в тайлах и не рассылаются через WebSocket. По сети идут только изменяемые сущности. 4. **Интерес по области (AOI).** Каждый игрок подписан на прямоугольник карты (вьюпорт плюс запас). Сервер шлёт ему дельты только для сущностей в этой области. Миллион сущностей к клиенту не уходит. 5. **LOD симуляции.** Вдали от всех наблюдателей жители симулируются грубо (потребности и расписания по шагам в десятки секунд, без пути по каждому тайлу), рядом с наблюдателем подробно. Это главный способ удержать миллион сущностей. ## 4. Формат данных города Сейчас `src/types.ts` описывает город одним объектом, всё генерируется в браузере. Для игры нужно разрезать: - **Манифест города**: границы, размер тайла, версия формата, список тайлов, метаданные. - **Тайлы**: сетка (например 256 x 256 м). В тайле: дороги, здания, этажи, интерьеры, мебель, индекс проходимости (navmesh или сетка). - **Формат файла**: бинарный (protobuf или FlatBuffers) со сжатием, чтобы читать и сервер, и клиент. На первом этапе допустим JSON + gzip, потом заменить. - **Навигация**: для сервера отдельно строится граф дорог и проходимая сетка, чтобы жители ходили, не загружая геометрию. **Интерьеры нужны серверу (решено).** Жители перемещаются по интерьерам и улицам и взаимодействуют с ними, поэтому сервер загружает полную проходимую геометрию: стены, двери, этажи, лестницы, мебель с типом, размером и точками взаимодействия (спать, сидеть, есть, работать). Загрузка по тайлам и лениво: интерьеры тайла нужны, только когда в нём есть наблюдатель или активные жители. Для навигации строятся два уровня: грубый граф (здания, улицы, входы) для дальних путей и детальная сетка внутри зданий и вокруг для ближних. Переходы между уровнями — двери и лестницы. **Улицы сейчас «пустоваты» (замечание автора).** До появления жителей на улицах нужно сгенерировать то, с чем они взаимодействуют снаружи: уличную мебель (скамейки, фонари, урны), остановки, парковки, дворы, переходы, входы в здания, места для торговли. Это отдельная задача генератора (этап 1б), см. раздел 9. ## 5. Модель симуляции (первая версия) Компоненты (примерный набор): - `Position`, `Velocity`, `TileId` (для быстрого AOI). - `Agent` (житель), `Needs` (сон, еда, работа, отдых), `Schedule`, `Home`, `Workplace`. - `Vehicle`, `Route`. - `Building`, `Room`, `Furniture` (служебные ссылки на статику). - `Dirty` (было ли изменение с прошлой рассылки). Системы (порядок и зависимости задаёт `Schedule`): 1. Время и календарь. 2. Потребности и выбор цели. 3. Построение пути, движение. 4. События (пожар, болезнь, происшествие), позже вмешательство игроков. 5. Сбор изменений и рассылка по AOI. Тик симуляции: 10 Гц фиксированный, скорость игры меняется множителем (как в RimWorld). Рассылка клиентам: 5–10 Гц, дельтами, клиент интерполирует. ## 6. Сеть и протокол Сообщения клиент → сервер: - `Join { room, client_version }` - `Subscribe { rect, lod }` — какую область смотрю. - `Ping`. - (позже) `Intervene { ... }`. Сообщения сервер → клиент: - `Welcome { room_info, tick, city_manifest_url }` - `Snapshot { tick, entities[] }` — полное состояние области при подписке. - `Delta { tick, spawned[], changed[], despawned[] }` — изменения области. - `Event { ... }` — редкие события (уведомления). Правила: - Позиции квантуются (например, в сантиметрах целым числом), идентификаторы компактные, дельты по полям. - Версия протокола в рукопожатии, несовместимый клиент получает понятную ошибку. - Ограничение размера и частоты запросов на клиента, ограничение размера области подписки. ## 7. Производительность: бюджеты Ориентиры для проверки (замерить и уточнить): - Тик комнаты с 1 млн сущностей: не более 50 мс на целевом сервере (при LOD, когда подробно симулируется лишь малая часть). - Рассылка: не более 100 КБ/с на клиента в среднем. - Память: порядка 100–200 байт на сущность в ECS, итого до 200 МБ на комнату без учёта статики. - Подключение клиента: менее 2 с до первой картинки (за счёт тайлов, а не снимка всего мира). Для проверки сразу закладываем нагрузочный стенд: генератор синтетической популяции (1 млн агентов на карте) и клиент-бот на 30 подключений. ## 8. Клиент (браузер) - `src/view` остаётся, но получает данные не из воркера генерации, а из загрузчика тайлов и WebSocket-клиента. - Слой состояния: хранит сущности вокруг камеры, применяет дельты, интерполирует позиции между тиками. - Рендер динамики: на первом этапе Canvas, дальше (при 10–50 тыс. видимых сущностей) WebGL с инстансингом. Геометрию тайлов можно кэшировать в офскрин-текстуры. - Режим «воспроизведение» (без сервера, текущий прототип) оставить как отладочный. ## 9. Этапы работ **Этап 0. Подготовка (1 неделя)** - [x] Зафиксировать решения этого документа, выбрать имя и структуру репозитория (монорепо: `server/`, `client/`, `tools/`, `proto/`). - [x] Определить минимальную сцену для проверки: один небольшой город, 1000 агентов, 1 клиент. **Этап 1. Тайловый формат (1–2 недели)** - [x] Экспорт готового города из текущего генератора в тайлы + манифест (`tools/export`). - [x] Клиент загружает город по тайлам, а не генерирует в браузере. - [x] Тесты формата, версия формата в манифесте. **Этап 1б. Обустройство улиц в генераторе (2 недели)** - [x] Уличные объекты и точки взаимодействия снаружи зданий: скамейки, фонари, остановки, парковки, дворы, переходы, входы. - [x] Единый реестр типов объектов с интерактивными слотами (как у мебели в `src/gen/catalog.ts`), общий для интерьеров и улиц. - [x] Цель: к моменту появления жителей снаружи есть куда идти и что делать. **Этап 2. Скелет сервера (1–2 недели)** - [x] Rust-проект: `axum`, WebSocket, протокол (`proto/`), одна комната, `bevy_ecs`. - [x] Загрузка манифеста и тайлов на сервере (навигационная сетка). - [x] Подключение клиента, подписка на область, снимок. **Этап 2б. Сервис генерации** - [x] Очередь с лимитами площади, зданий, одновременных задач и таймаутом. - [x] Изолированный TS-воркер, прогресс, кэш по нормализованному bbox, seed и версии. - [x] `CreateCity` / `CityJob` в общей Protobuf-схеме и интерфейс создания города. - [x] Сквозной тест запроса, отдельного Node-процесса, экспорта, открытия комнаты и сохранения без внешней сети. **Этап 3. Простая симуляция (2–3 недели)** - [x] Агенты ходят по дорогам между домом и работой по расписанию. - [x] Дельты по AOI, интерполяция на клиенте. - [x] Детерминизм: seed комнаты, воспроизводимые тики. - [x] Бинарные атомарные снимки, ротация, ленивое открытие сохранённых комнат и тест эквивалентности после восстановления. **Этап 4. Масштаб и LOD (2–3 недели)** - [x] Нагрузочный стенд (1 млн агентов, 30 клиентов). - [x] Профилирование, LOD симуляции, параллелизм систем. Реализованы грубый шаг дальних маршрутов, общий пространственный индекс, стабильный LOD репликации и параллельная обработка движения через пул ECS. Построение индекса зависит от движения и идёт следующим шагом. - [x] Оптимизация протокола и рендера. Бинарные дельты, квантизация, объединение пропущенных обновлений под бюджетом трафика, ограничение видимых сущностей и кэш тайлов; динамика пока на Canvas. **Этап 5. Базовый игровой слой** - [x] Потребности, события, экономика. Усреднённые голод, усталость и настроение населения; сон, питание, производство еды и доход казны. События урожая, болезни и дождя воспроизводятся из seed и игрового времени. Индивидуальный ИИ жителей пока остаётся расписанием дом–работа. - [x] Ограниченное влияние игроков (команды, права, очереди, лимиты). Покупка запасов, общий обед и отдых; одинаковые права подключённых игроков, цена из общей казны, ограниченная очередь, общий часовой игровой интервал и интервал сессии. Пауза/скорость остаются голосованием. - [x] Сохранения, восстановление комнат, админка. Схема v2 сохраняет игровой слой и очередь, v1 мигрируется при открытии. Добавлены общая квота снимков, защита прежних файлов при переполнении и админский интерфейс/API с отдельным токеном оператора. **Этап 6. Эксплуатация** - [x] Подготовка Compose, мониторинг (метрики тика, число сущностей, трафик), логи. По уточнению автора этап 6 ограничен подготовкой Compose. Добавлены постоянные тома, healthcheck, лимиты, SIGTERM с сохранением, ротация логов и опциональный Prometheus с правилами предупреждений. Проверка `docker compose --profile monitoring config --quiet` проходит. Docker Engine выключен: сборка и запуск контейнера не проверены. - [x] Создание комнат по запросу (выбор города), лимиты ресурсов. Проверка минимальной сцены: `npm run demo`, `npm run build`, `npm run server:dev`, затем `http://localhost:3000/?play`. Имя репозитория остаётся `citygen`; генератор и существующий рендер сохраняют свои пути в `src/`, новые сетевые модули находятся в `client/`. Состояние и ограничения реализации описаны в `server/README.md`, запуск Compose — в `deploy/README.md`. Этап 5 реализует базовую городскую модель, этап 6 — подготовку Compose в согласованном объёме. ## 10. Риски | Риск | Что делаем | |---|---| | 1 млн сущностей не успевают в тик | LOD, чанки, разреженные системы; замер на этапе 4, а не в конце | | Объём рассылки вырастет | AOI, квантование, дельты, приоритет близким к камере | | Размер тайлов и время загрузки | Компактный бинарный формат, сжатие, ленивая загрузка тайлов по камере | | Крутая кривая Rust для команды | Держать код простым, избегать лишних абстракций, начать с одной комнаты | | Изменение формата данных ломает сохранения | Версия формата, миграции, тесты совместимости | | Дублирование типов между Rust и TS | Единая схема в `proto/`, кодогенерация | ## 11. Принятые решения, допущения и открытые вопросы Принятые решения: 1. **Интерьеры нужны на сервере** (раздел 4): жители ходят по интерьерам и улицам и взаимодействуют с ними. 2. **Без аккаунтов.** Вход по нику; персистентности игрока между комнатами пока нет. Ник уникален в пределах комнаты, идентификатор сессии выдаёт сервер. 3. **Создание комнат игроком** с лимитом из конфига сервера (`max_rooms`, по умолчанию 5). При достижении лимита создание отклоняется с понятной ошибкой. Пустые комнаты закрываются по таймауту (параметр конфига), чтобы не занимать слот. 4. **Время общее для комнаты.** Пауза и смена скорости — через голосование: предложение принимается, если «за» большинство подключённых игроков (более половины). Один игрок в комнате решает сам. Нужны: таймаут голосования, один активный вопрос за раз, защита от спама предложениями. 5. **Только десктопные браузеры.** Мобильные не поддерживаем, рендер и управление рассчитываем на мышь и клавиатуру. 6. **`max_rooms` — лимит на весь сервер** одновременно открытых комнат (подтверждено). 7. **Игрок создаёт свой город**: выбирает область на карте и запускает генерацию (раздел 12). Каталог готовых городов тоже остаётся. 8. **Владельца комнаты нет**: создатель не может закрыть комнату, выгнать игрока или поставить пароль. Комната закрывается только по таймауту пустоты. 9. **Комнаты переживают перезапуск сервера**: состояние сохраняется на диск (раздел 13). Допущения (если неверны, скажи — поправлю): 1. Серверная симуляция детерминируема от seed. Отдельный вопрос, нужна ли строгая детерминированность (для повторов и отладки). 2. Деплой на один сервер, комнаты как задачи внутри одного процесса. Горизонтальное масштабирование — позже. 3. Бюджет железа: современный многоядерный сервер, 8+ ядер. Открытые вопросы: 1. Лимит размера области для генерации (площадь, число зданий) и лимит одновременных генераций. Предварительно: берём из конфига, значения подобрать после замеров на этапе 1. 2. Что делать с комнатой без владельца, если она не закрывается: пустая комната закрывается через N минут, но сохраняется на диск и может быть открыта заново? Предварительно: да, закрытая комната хранится как снимок, пока есть место. 3. Нужна ли защита от злоупотреблений при отсутствии аккаунтов (лимит создания по IP, антиспам)? Предварительно: да, простой лимит по IP. ## 12. Создание города игроком Генерация города — тяжёлая работа (запросы Overpass, построение зданий и интерьеров), поэтому выполняется отдельным сервисом, а не в потоке комнаты. Процесс: 1. Игрок на карте выбирает прямоугольник (в клиенте уже есть выбор области: `src/view/picker.ts`, ограничения размера в `src/main.ts`). 2. Клиент отправляет серверу `CreateCity { bbox, name }`. Сервер проверяет лимит площади, лимит очереди и лимит комнат (`max_rooms`). 3. Задача попадает в очередь. Воркер (текущий TS-генератор как отдельный Node-процесс, использующий `scripts/cli.ts` и `src/gen`) запрашивает Overpass, генерирует город, экспортирует тайлы и манифест (этап 1) в хранилище. 4. Клиент видит прогресс (в очереди, загрузка OSM, генерация, экспорт). По готовности город попадает в каталог игрока и создаётся комната. 5. Ошибки Overpass (таймауты, лимиты) обрабатываются повторами и понятными сообщениями; уже есть наработки в `src/osm/overpass.ts`. Требования: - Города кэшируются по нормализованному bbox и версии генератора, чтобы одну область не генерировать дважды. - Ограничения: максимальная площадь и число зданий, максимум одновременных задач (конфиг), таймаут задачи. - Версия формата и генератора пишется в манифест; при смене версии кэш инвалидируется. - Генерация должна быть детерминированной от (bbox, seed, версия), чтобы город можно было воспроизвести. ## 13. Сохранение и восстановление комнат - **Снимки** `World` раз в N минут и при остановке: сущности, компоненты, время игры, seed, состояние голосований не сохраняется (сбрасывается). - **Формат**: бинарный, версионируемый (протокол из `proto/`), атомарная запись во временный файл с переименованием, хранение последних K снимков. - **Восстановление**: при старте сервера список комнат читается с диска, комнаты открываются лениво при первом подключении игрока, чтобы не держать в памяти все. - **Миграции**: при смене схемы компонентов снимок либо мигрируется, либо комната помечается несовместимой с понятным сообщением. - **Целостность**: комната ссылается на город по идентификатору и версии манифеста; удаление или смена города при существующих комнатах запрещена. Дополнение к этапам: - Этап 2: пустая комната сохраняется и восстанавливается (скелет сохранения). - Этап 3: сохранение/загрузка простой симуляции, тест «остановил — запустил — состояние совпадает». - Этап 5: миграции, ротация снимков, лимиты места. - Новый этап **2б. Сервис генерации** (2 недели): очередь задач, воркер на базе `scripts/cli.ts`, кэш по bbox, прогресс и лимиты, протокол `CreateCity` и ответы о статусе. ## 14. Проверки реализации - TypeScript: `npm run typecheck`, `npm test`, `npm run build`. - Rust: `npm run server:test`; отдельно проверяются навигация через двери, точность пространственного индекса, детерминизм, атомарные снимки и ротация. - Интеграция: 30 настоящих WebSocket-подключений, отказ 31-му, голосование 15/30 против 16/30, повторное подключение после восстановления. - Очередь: Protobuf-запрос → отдельный TS-процесс с локальной OSM-выгрузкой → экспорт с контрольными суммами → открытие комнаты → сохранение. Внешний Overpass в тесте не используется. - Chrome: каталог, вход, загрузка тайлов, этажи и пауза через голосование. Повторяемая проверка: `scripts/browser-smoke.mjs` при установленном Playwright. - Реальная нагрузка, 2026-10-30: Бальцерс, 1 000 000 жителей, 30 WebSocket-клиентов, 20 с, фоновые сохранения каждые 2 с. Выборка длительности тиков через метрики: p95 36,5 мс; максимум всех тиков по счётчику комнаты 44,7 мс. Средний трафик 4,8 КБ/с на клиента, максимальный 44,7 КБ/с. Ошибок, разрывов и срабатываний ограничения трафика не было. Это короткий локальный тест с небольшими AOI, а не гарантия для любого города, плотности, диска и целевого сервера. Пиковая память процесса со статикой и буферами — около 272 МиБ; память одной ECS отдельно не измерялась. - Браузер: повторный вход в сохранённую миллионную комнату до загруженной области — 1,31 с в локальном Chrome. - Этапы 5–6: 17 TS-тестов, 11 Rust unit-тестов и 2 интеграционных теста проходят. Проверены миграция v1→v2, сохранение очереди команды, детерминизм экономики/событий, влияние потребностей на производство, расход еды, квота без потери последнего снимка, отказ без админского токена и подтверждение записи через API. - Chrome: отображение казны/еды/потребностей, команда «Обед» со списанием ресурсов и снижением голода, пауза, этаж/масштаб, админское сохранение. Загрузка области — 1,12 с локально. Дальнейшее развитие за пределами базового слоя: индивидуальные потребности и выбор целей, детальные переходы между этажами, использование мебели и уличных слотов жителями, транспорт. Подготовленный Compose ещё нужно собрать и запустить на Docker-хосте; внешнее развёртывание в текущий запрос не входит.