Files
h-school/docs/design/defs.md
T

235 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Defs, карта и моды
Договорённость на ближайшее планирование, не текущий код.
Экран игрока: [`near-term.md`](near-term.md). Потоки и ECS: [`runtime.md`](runtime.md).
Как устроен хост сейчас: [`../architecture.md`](../architecture.md).
Идея как у RimWorld: **ядро — код систем**, содержание игры — **данные (defs)**. Новый стул, кабинет
или действие, которое уже умеет код, появляются без пересборки `HSchool.Simulation`. Сборка нужна,
только когда появляется *новый вид поведения*, которого нет ни в одной системе.
Это не локальная RimWorld: сервер авторитетен, клиент — браузер. Папка мода живёт **у сервера**.
Клиент не грузит defs и не считает ходы; он рисует снимки, которые сервер собрал из defs + инстанса
карты.
## Два слоя, не один
| Слой | Что это | Пример |
| --- | --- | --- |
| **Def** | Тип, каталог | «Стул», «Сесть», «Кабинет директора» |
| **Инстанс** | Конкретная школа / карта | *этот* стул директора в *этом* кабинете, дверь в *этот* коридор |
Дерево в UI — вид на инстанс. Граф проходов (из кабинета в коридор и обратно) тоже **инстанс**:
в def кабинета нельзя честно написать «ведёт в коридор №3», этого коридора ещё нет. В def —
какие предметы и работы *характерны* для типа помещения; на карте — какие узлы реально стоят
и какими рёбрами связаны.
Должность и вакансия рождаются из инстанса: поставили кабинет директора → в школе появилась
должность и незакрытая вакансия. Снесли кабинет — вакансия уходит. Def помещения говорит
*какую* должность этот тип создаёт, карта говорит *что уже построено*.
## Виды def (первая нарезка)
Имена предварительные, смысл — ваш.
1. **ActionDef** — глагол: сесть, перенести. Код системы знает, как исполнить известный `defName`.
2. **ThingDef** — предмет. Ссылается на действия, которые с ним допустимы (`Chair``Sit`).
3. **PositionDef** — должность (директор). Вакансия — не def, а состояние школы.
4. **WorkDef** — работа, которую можно вести в помещении (работа директора, урок, обход школы).
5. **RoomDef** — тип помещения. Слоты предметов, должности, которые он открывает, список WorkDef.
6. **BuildingDef** / группировка этажа — если зданию нужны свои свойства; иначе этаж может быть
только узлом карты без отдельного def.
Связи «из A можно попасть в B» **не** поле RoomDef, а рёбра карты. Иначе дефолтная и пользовательская
карта не смогут расставить одни и те же типы комнат по-разному.
## Как выглядит дефолтная карта
Это файл **раскладки**, не каталог типов. Он ссылается на `defName` и задаёт id узлов.
Иерархия дерева (она же закрывает вопрос «из чего дерево»):
```
территория школы
└── здание (их может быть несколько)
└── этаж (если в здании больше одного; иначе этаж можно не показывать)
└── помещение
```
Граф локаций — те же помещения плюс рёбра в обе стороны (кабинет ↔ коридор). Дерево — группировка
для UI, граф — куда можно перейти и откуда берутся «соседние» события.
В процессе разработки кладём одну ванильную раскладку в Core. При создании школы игрок может
изменить её в редакторе или стереть и собрать свою. Редактор оперирует инстансом и **выбирает
типы из каталога def**, а не рисует свободную геометрию (геометрии в этом горизонте нет).
## Формат: JSONC, не новый язык
Рекомендация: **JSON с комментариями и хвостовыми запятыми** (JSONC). `System.Text.Json` это уже
умеет (`ReadCommentHandling.Allow`, `AllowTrailingCommas`). Отдельный YAML/TOML/XML не даёт
выигрыша, а RimWorld-XML здесь ни к чему — патчи можно сделать проще.
Почему не «чистый JSON»: defs пишут люди, комментарии обязательны. Почему не YAML: пробелы и
два парсера в голове. Почему не свой формат: его придётся документировать сильнее, чем игру.
Один файл — один def или небольшая пачка одного вида (`things/chair.jsonc`). Ссылки — строки
`defName`, резолв после загрузки всего каталога (как у RimWorld: сначала прочитать, потом связать).
Наследование по желанию, как `ParentName`: `{ "defName": "OfficeChair", "parent": "Chair" }`.
Не обязательно в первом срезе; без него можно жить, пока стульев мало.
Патчи модов (добавить действие к ванильному стулу, не копируя файл) — отдельным видом файлов
позже. Сначала: новые defs + замена дефолтной карты целиком. Частичные патчи — когда появится
второй мод, которому это реально нужно.
Подписи содержания живут **не в def**, а в `localizations/` мода. В def остаётся `defName`
(и при необходимости ключ, если он не совпадает с именем). Черновик без вшитых ru/en:
```jsonc
// ActionDef — заготовка: каталог знает глагол, исполнения в этом горизонте нет
{ "defName": "Sit" }
// ThingDef
{ "defName": "Chair", "actions": ["Sit"] }
// RoomDef: тип, не конкретный кабинет на карте
{
"defName": "PrincipalsOffice",
"slots": [
{ "key": "directorChair", "thing": "DirectorsChair" },
{ "key": "desk", "thing": "Desk" },
{ "key": "guestChair", "thing": "Chair", "count": 2 },
],
"positions": ["Principal"],
"works": ["PrincipalOfficeWork", "TeachLesson", "WalkSchool"],
}
// Кусок карты (инстанс)
{
"rooms": [
{
"id": "principals-office",
"def": "PrincipalsOffice",
"building": "main",
"floor": "1",
"links": ["corridor-1"],
},
],
}
```
## Что остаётся кодом
Def говорит «у стула есть Sit». Система `Sit` в C# знает правила: кто может сесть, сколько это
длится, что происходит с персонажем. Новый предмет с тем же `Sit` — только JSON. Новый глагол
«взломать замок», которого нет в коде — либо обобщённый исполнитель (долго и скользко), либо
сборка с новой системой.
Пока держим правило RimWorld: **данные компонуют известные глаголы; неизвестный глагол = код.**
Скриптовый язык внутри JSON в этом горизонте не заводим.
В первой живой версии ActionDef и ссылки с предметов — **заготовки**: они есть в каталоге и на
карте, системы их ещё не исполняют, игрок не отдаёт приказ «сесть». Имеет смысл уже показать
список в панели локации (чтобы каркас был правдой), но не симулировать.
## Карта: пустые комнаты и связность
Помещение может быть **пустым** (ни одного предмета в слотах). Не может быть **висячим**: каждое
помещение связано хотя бы с одним другим, и карта в целом — один связный неориентированный граф
(рёбра двусторонние).
Исключение: школа из **одной** комнаты — граф из одной вершины, рёбер нет, это допустимо.
Два здания без перехода между ними — нет: нужен общий узел (коридор, двор, территория как
локация) или явное ребро.
Сервер отвергает раскладку с изолированной комнатой, ребром в несуществующий id и `defName`,
которого нет в каталоге *выбранных для этой школы* модов.
## Редактор — только при создании
В этом горизонте карту меняют **до** того, как школа пошла жить: в диалоге создания. После
create раскладка замораживается вместе с набором модов. Построить/снести в уже идущей школе —
отдельный разговор (это уже игровые приказы, не редактор).
Порядок в UI: выбрать моды → получить каталог (типы комнат/предметов) → править или стереть
дефолтную карту → имя, дата, создать. Редактор без списка модов не знает, какие RoomDef существуют.
## Папка мода и локализации
Моды — подпапки на диске сервера, например `mods/<id>/`. Ядро — тоже мод (всегда включён или
выбирается отдельно, см. открытые вопросы), лежит рядом, чтобы путь загрузки был один.
```
mods/
core/
defs/ # JSONC типов
localizations/
ru.jsonc
en.jsonc
maps/
default.jsonc # ванильная раскладка
furniture-pack/
defs/
localizations/
ru.jsonc
en.jsonc
```
Ключи локализации — по `defName` (и суффиксам вроде `.label`, если понадобятся несколько строк
на тип). Нет ключа — в UI показывается `defName`, это сразу видно в тесте мода.
Хром клиента (меню, кнопки) по-прежнему в `src/HSchool.Client/src/i18n/`. Строки содержания
сервер резолвит из `localizations/` включённых модов под язык клиента и кладёт в снимок / в
ответ каталога для редактора. Браузер файлы модов не читает.
При создании школы в UI показывают список папок из `mods/` (кроме того, что решим всегда
включать). Выбранный набор уходит вместе с раскладкой. Работник школы грузит **этот** набор
и замораживает каталог. Новые файлы на диске влияют только на следующие школы; список модов
для диалога можно сканировать при `GET`, без рестарта процесса.
## Где это живёт в решении
- Каталог def и загрузка папок — `HSchool.Simulation` (без ASP.NET, без сокетов). Тесты читают
фикстуры и резолвят ссылки. Каталог **на школу**: зависит от модов, выбранных при create.
- Инстанс карты — часть `School`, рядом с часами и ECS. Свой `World` у каждой школы; свой поток —
см. [`runtime.md`](runtime.md).
- Валидация раскладки с клиента — на сервере: неизвестный `defName`, ребро в никуда, несвязный
граф, изолированная комната (если комнат больше одной).
- Клиент редактора — DOM в диалоге создания; тело create = имя, дата, список модов, раскладка.
Сканирование `mods/` — при запросе списка и при создании школы, не каждый тик. Горячая подмена
каталога у уже живой школы в этом горизонте не нужна.
## Советы, которые стоит принять сразу
- Не класть граф проходов в RoomDef — только в карту.
- Не слать весь каталог и всю карту 20 раз в секунду: дерево и состав комнаты — при изменении
или при открытии школы; часы как сейчас.
- Редактор только в create: шлёт раскладку и список id модов, не пути на диск и не C#.
- Слоты в RoomDef — чертёж; на карте слот можно не заполнять (пустая комната).
- Локали не смешивать с defs: иначе мод на третьем языке правит типы.
## Зафиксировано этим разговором
| Тема | Решение |
| --- | --- |
| Содержание игры | Максимально defs, ядро — системы под известные глаголы |
| Формат (предложение) | JSONC, ссылки строками `defName` |
| Карта | Инстанс: территория → здания → этажи → помещения + граф связей |
| Дефолтная карта | Одна ванильная раскладка Core; при создании можно править или стереть |
| Редактор | Только в диалоге создания школы |
| Пустая комната | Можно; граф должен быть связным (одна комната — ок) |
| Действия | Заготовки в def, без исполнения и без приказов |
| Моды | `mods/<id>/` на диске сервера, выбор в UI при создании; каталог замораживается |
| Локализация модов | `mods/<id>/localizations/{ru,en}.jsonc`, не внутри def |
| Код модов (DLL) | Не в этом горизонте |
| Поток / ECS | Отдельный `World` обязательно; отдельный поток на школу — цель, см. [`runtime.md`](runtime.md) |
## Ещё не решено
1. `core` всегда включён и его нельзя снять в UI, или это такой же мод в списке?
2. Территория / двор — отдельная ходибельная локация (чтобы связывать здания), или только группировка в дереве?
3. Этаж без отдельного def — ок, пока у этажа нет своих свойств?
4. Порядок модов в списке при create влияет на коллизии `defName` (последний победил) — так и делаем?
5. Сохранение школы на диск — здесь или после первого живого среза? (набор модов и раскладку всё равно надо будет сериализовать вместе со школой, когда сохранения появятся)