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

17 KiB
Raw Blame History

Defs, карта и моды

Договорённость на ближайшее планирование, не текущий код. Экран игрока: near-term.md. Потоки и ECS: runtime.md. Как устроен хост сейчас: ../architecture.md.

Идея как у RimWorld: ядро — код систем, содержание игры — данные (defs). Новый стул, кабинет или действие, которое уже умеет код, появляются без пересборки HSchool.Simulation. Сборка нужна, только когда появляется новый вид поведения, которого нет ни в одной системе.

Это не локальная RimWorld: сервер авторитетен, клиент — браузер. Папка мода живёт у сервера. Клиент не грузит defs и не считает ходы; он рисует снимки, которые сервер собрал из defs + инстанса карты.

Два слоя, не один

Слой Что это Пример
Def Тип, каталог «Стул», «Сесть», «Кабинет директора»
Инстанс Конкретная школа / карта этот стул директора в этом кабинете, дверь в этот коридор

Дерево в UI — вид на инстанс. Граф проходов (из кабинета в коридор и обратно) тоже инстанс: в def кабинета нельзя честно написать «ведёт в коридор №3», этого коридора ещё нет. В def — какие предметы и работы характерны для типа помещения; на карте — какие узлы реально стоят и какими рёбрами связаны.

Должность и вакансия рождаются из инстанса: поставили кабинет директора → в школе появилась должность и незакрытая вакансия. Снесли кабинет — вакансия уходит. Def помещения говорит какую должность этот тип создаёт, карта говорит что уже построено.

Виды def (первая нарезка)

Имена предварительные, смысл — ваш.

  1. ActionDef — глагол: сесть, перенести. Код системы знает, как исполнить известный defName.
  2. ThingDef — предмет. Ссылается на действия, которые с ним допустимы (ChairSit).
  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:

// 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.
  • Валидация раскладки с клиента — на сервере: неизвестный 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

Ещё не решено

  1. core всегда включён и его нельзя снять в UI, или это такой же мод в списке?
  2. Территория / двор — отдельная ходибельная локация (чтобы связывать здания), или только группировка в дереве?
  3. Этаж без отдельного def — ок, пока у этажа нет своих свойств?
  4. Порядок модов в списке при create влияет на коллизии defName (последний победил) — так и делаем?
  5. Сохранение школы на диск — здесь или после первого живого среза? (набор модов и раскладку всё равно надо будет сериализовать вместе со школой, когда сохранения появятся)