22 KiB
Defs, карта и моды
Договорённость на ближайшее планирование, не текущий код.
Экран игрока: near-term.md. Потоки: runtime.md.
Проекты: projects.md. Как устроен хост сейчас: ../architecture.md.
Идея как у RimWorld: ядро — код систем, содержание игры — данные (defs). Новый стул, кабинет
или действие, которое уже умеет код, появляются без пересборки HSchool.Simulation. Сборка нужна,
только когда появляется новый вид поведения, которого нет ни в одной системе.
Это не локальная RimWorld: сервер авторитетен, клиент — браузер. Папка мода живёт у сервера. Клиент не грузит defs и не считает ходы; он рисует снимки, которые сервер собрал из defs + инстанса карты.
Два слоя, не один
| Слой | Что это | Пример |
|---|---|---|
| Def | Тип, каталог | «Стул», «Сесть», «Кабинет директора» |
| Инстанс | Конкретная школа / карта | этот стул директора в этом кабинете, дверь в этот коридор |
Дерево в UI — вид на инстанс. Граф проходов (из кабинета в коридор и обратно) тоже инстанс: в def кабинета нельзя честно написать «ведёт в коридор №3», этого коридора ещё нет. В def — какие предметы и работы характерны для типа помещения; на карте — какие узлы реально стоят и какими рёбрами связаны.
Должность и вакансия рождаются из инстанса: поставили кабинет директора → в школе появилась должность и незакрытая вакансия. Снесли кабинет — вакансия уходит. Def помещения говорит какую должность этот тип создаёт, карта говорит что уже построено.
Виды def (первая нарезка)
Имена предварительные, смысл — ваш.
- ActionDef — глагол: сесть, перенести. Код системы знает, как исполнить известный
defName. - ThingDef — предмет. Ссылается на действия, которые с ним допустимы (
Chair→Sit). - PositionDef — должность (директор). Вакансия — не def, а состояние школы.
- WorkDef — работа, которую можно вести в помещении (работа директора, урок, обход школы).
- RoomDef — тип помещения. Слоты предметов, должности, которые он открывает, список WorkDef.
- BuildingDef — тип корпуса на территории (учебный, спортзал). Тонкий: редактор ставит тип здания, моды добавляют новые корпуса.
- FloorDef — тип этажа (типовой, подвал, чердак). Сейчас тонкий:
defNameи локаль, без правил. Узел карты: id,def, родитель-здание, при необходимости номер/подпись («2»). Задел: позже на def повесятся свойства этажа, не меняя форму ссылки на карте. - TerritoryDef — тип двора. Корень дерева и узел графа, такой же тонкий задел. В
coreхотя бы один. Клик по двору — те же секции панели, что у комнаты.
Связи «из A можно попасть в B» не поле RoomDef, а рёбра карты. Иначе дефолтная и пользовательская карта не смогут расставить одни и те же типы комнат по-разному.
Как выглядит дефолтная карта
Это файл раскладки, не каталог типов. Он ссылается на defName и задаёт id узлов.
Иерархия дерева (она же закрывает вопрос «из чего дерево»):
территория школы ← TerritoryDef, корень дерева и узел графа
└── здание
└── этаж ← FloorDef + 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. Загрузчик Content идёт так:
- Прочитать defs всех пакетов по порядку (
core, затем моды). ПовторdefName— последний победил, warning. - Разрешить наследование.
- Применить патчи пакетов в том же порядке.
- Резолв ссылок (
actions, слоты). Abstract def на карту ставить нельзя.
Это база: новые поля у def и новые op у патча добавляются в Content, папки не меняются.
Наследование
Как ParentName у RimWorld:
{ "defName": "FurnitureBase", "abstract": true }
{ "defName": "Chair", "parent": "FurnitureBase", "actions": ["Sit"] }
{ "defName": "OfficeChair", "parent": "Chair" }
- Нет поля у ребёнка — берётся у родителя (цепочка до корня).
- Поле есть — заменяет целиком, в том числе массивы. Дописать элемент в массив родителя — не наследованием, а патчем.
abstract: true— только база, на карте и в редакторе не выбирается.- Родитель другого вида (Thing → Room) или цикл — ошибка загрузки каталога.
- Локаль: ключ
defName, если нет — вверх поparent.
Патчи
Чтобы не копировать ванильный стул ради одного действия. Лежат в patches/, не в defs/.
Цель — defName уже после наследования. Указатель поля — JSON Pointer.
{
"target": "Chair",
"ops": [
{ "op": "add", "path": "/actions/-", "value": "Inspect" },
{ "op": "replace", "path": "/slots/0/thing", "value": "DirectorsChair" },
{ "op": "remove", "path": "/works/1" },
],
}
В этом срезе три операции: add (в массив - = в конец), replace, remove. Неизвестный
op, битый pointer или нет target — каталог не грузится (школу с этим набором модов не
поднимать). Новые операции потом просто появляются в enum, файлы модов те же.
Патч не меняет раскладку карты: дефолтная карта по-прежнему файл maps/ (last-wins на файл,
если мод положит свой default.jsonc). Патчи — про типы.
Подписи содержания живут не в def, а в localizations/ мода. В def остаётся defName
(и при необходимости ключ, если он не совпадает с именем). Черновик без вшитых ru/en:
// 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 и ссылки с предметов — заготовки: они есть в каталоге и на карте, системы их ещё не исполняют, игрок не отдаёт приказ «сесть». В панели локации показываем имена действий у стоящих предметов. Персонажи и «А говорит с Б» — секции панели, списки пустые. Должности берём из RoomDef поставленного помещения (вакансии без людей).
Снимок карты на клиент
При OpenSchool сервер один раз отдаёт дерево и состав всех узлов (двор, корпуса, этажи,
помещения). Подписи — язык из Hello (ru | en). Каталог для редактора — тот же код языка
в ?lang= у HTTP. Accept-Language не используем: переключатель RU/EN в подвале с ним разъедется.
Клиент сам выбирает узел; секции панели одни и те же. Часы по-прежнему 20 Гц. Карта после create в этом срезе не меняется, повторно слать не нужно. Hello с locale — смена раскладки кадра, в том же коммите bump версии протокола.
Карта: пустые комнаты и связность
Помещение может быть пустым (ни одного предмета в слотах). Карта в целом — один связный неориентированный граф (рёбра двусторонние). Территория в нём всегда есть: это и корень дерева, и двор.
Изолированный узел запрещён. Практический минимум: двор + хотя бы одно помещение с ребром во двор (или цепочка помещений, которая в итоге выходит во двор). Школа «одна комната без двора» в этой модели не собирается — двор не выкидывается. Два здания без пути через двор (или другое ребро) — тоже нет.
Сервер отвергает раскладку с изолированной комнатой, ребром в несуществующий id и defName,
которого нет в каталоге выбранных для этой школы модов.
Редактор — только при создании
В этом горизонте карту меняют до того, как школа пошла жить: в диалоге создания. После create раскладка замораживается вместе с набором модов. Построить/снести в уже идущей школе — отдельный разговор (это уже игровые приказы, не редактор).
Порядок в UI: выбрать моды → получить каталог (типы комнат/предметов) → править или стереть дефолтную карту → имя, дата, создать. Редактор без списка модов не знает, какие RoomDef существуют.
Папка мода и локализации
Моды — подпапки на диске сервера, например mods/<id>/. Ядро — мод core: тот же формат папки,
но всегда включён, в UI создания его нельзя снять. Остальные папки — чекбоксы.
mods/
core/
defs/ # JSONC типов
localizations/
ru.jsonc
en.jsonc
maps/
default.jsonc # ванильная раскладка
furniture-pack/
defs/
patches/ # правки чужих def, не копии
localizations/
ru.jsonc
en.jsonc
Порядок загрузки: сначала core, потом выбранные моды в том порядке, в каком они стоят в
запросе create. Одинаковый defName или ключ локали — побеждает последний. Имеет смысл
писать предупреждение в лог, не молча, чтобы конфликт модов было видно.
Ключи локализации — по defName (и суффиксам вроде .label, если понадобятся несколько строк
на тип). Нет ключа — в UI показывается defName, это сразу видно в тесте мода.
Хром клиента (меню, кнопки) по-прежнему в src/HSchool.Client/src/i18n/. Строки содержания
сервер резолвит из localizations/ включённых модов под язык клиента и кладёт в снимок / в
ответ каталога для редактора. Браузер файлы модов не читает.
В диалоге создания показывают core как включённый и заблокированный плюс список остальных
папок. Тело create несёт только дополнительные id (сервер всё равно подставит core первым).
Работник школы грузит этот набор и замораживает каталог. Новые файлы на диске влияют только на
следующие школы; список модов для диалога можно сканировать при GET, без рестарта процесса.
Где это живёт в решении
- Каталог def, раскладка и валидация — HSchool.Content, см.
projects.md.HSchool.Simulationтолько пользуется уже собранным каталогом. Путиmods/иsaves/— Server. - Инстанс карты (граф) — тип из Content, лежит в
School(Simulation) вместе с часами и ECS. Поток и файл сейва — Server, см.runtime.mdиprojects.md. Сейв: раскладка, id модов, часы, имя; каталог снова с диска при загрузке. Не писать каждый тик. - Валидация раскладки — Content; Server только вызывает её на теле create.
- Клиент редактора — DOM в диалоге создания; тело create = имя, дата, список модов, раскладка.
Сканирование mods/ — при запросе списка и при создании школы, не каждый тик. Горячая подмена
каталога у уже живой школы в этом горизонте не нужна.
Советы, которые стоит принять сразу
- Не класть граф проходов в RoomDef — только в карту.
- Не слать весь каталог и всю карту 20 раз в секунду: дерево — один снимок при открытии.
- Редактор только в create: шлёт раскладку и список id модов, не пути на диск и не C#.
- Слоты в RoomDef — чертёж; на карте слот можно не заполнять (пустая комната).
- Локали не смешивать с defs: иначе мод на третьем языке правит типы.
Зафиксировано этим разговором
| Тема | Решение |
|---|---|
| Содержание игры | Максимально defs, ядро — системы под известные глаголы |
| Формат (предложение) | JSONC, ссылки строками defName |
| Карта | Инстанс: территория → здания → этажи → помещения + граф связей |
| Дефолтная карта | Одна ванильная раскладка Core; при создании можно править или стереть |
| Редактор | Только в диалоге создания школы |
| Пустая комната | Можно; граф связный, двор всегда в графе |
| Действия | Заготовки в def, без исполнения и без приказов |
| Моды | mods/<id>/; core всегда первый и не снимается; остальные — UI create |
| Коллизии имён | Последний пакет победил (defs и локали), warning в лог |
| Наследование | parent, abstract; поле ребёнка заменяет, не мержит массивы |
| Патчи | patches/ после наследования; ops: add / replace / remove; неизвестный op — ошибка |
| Территория | TerritoryDef: корень дерева и ходибелый двор |
| Панель узла | Одни секции у двора, корпуса, этажа, комнаты |
| Язык | ?lang= и Hello; не Accept-Language |
| Локализация модов | mods/<id>/localizations/{ru,en}.jsonc, не внутри def |
| Этаж | FloorDef, тонкий; правила этажа — поля def позже |
| Здание | BuildingDef — тип корпуса |
| Сохранения | Файл школы: раскладка, id модов, часы, имя, пауза/скорость; каталог с диска при загрузке |
| Поток / ECS | Основа среза: один поток и один World на школу, см. runtime.md |
| Код модов (DLL) | Не в этом срезе |