235 lines
17 KiB
Markdown
235 lines
17 KiB
Markdown
# 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. Сохранение школы на диск — здесь или после первого живого среза? (набор модов и раскладку всё равно надо будет сериализовать вместе со школой, когда сохранения появятся)
|