Remove off-queue.md and add new design documentation files including README.md, defs.md, near-term.md, projects.md, runtime.md, people.md, staffing.md, and schedule.md to outline the structure and agreements for the game's design phases.
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
# Defs, карта и моды
|
||||
|
||||
Договорённость на ближайшее планирование, не текущий код.
|
||||
Экран игрока: [`near-term.md`](near-term.md). Потоки: [`runtime.md`](runtime.md).
|
||||
Проекты: [`projects.md`](projects.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** — тип корпуса на территории (учебный, спортзал). Тонкий: редактор ставит *тип*
|
||||
здания, моды добавляют новые корпуса.
|
||||
7. **FloorDef** — тип этажа (типовой, подвал, чердак). Сейчас тонкий: `defName` и локаль, без правил.
|
||||
Узел карты: id, `def`, родитель-здание, при необходимости номер/подпись («2»). Задел: позже на
|
||||
def повесятся свойства этажа, не меняя форму ссылки на карте.
|
||||
8. **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 идёт так:
|
||||
|
||||
1. Прочитать defs всех пакетов по порядку (`core`, затем моды). Повтор `defName` — последний
|
||||
победил, warning.
|
||||
2. Разрешить **наследование**.
|
||||
3. Применить **патчи** пакетов в том же порядке.
|
||||
4. Резолв ссылок (`actions`, слоты). Abstract def на карту ставить нельзя.
|
||||
|
||||
Это база: новые поля у def и новые `op` у патча добавляются в Content, папки не меняются.
|
||||
|
||||
### Наследование
|
||||
|
||||
Как `ParentName` у RimWorld:
|
||||
|
||||
```jsonc
|
||||
{ "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.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"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:
|
||||
|
||||
```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 и ссылки с предметов — **заготовки**: они есть в каталоге и на
|
||||
карте, системы их ещё не исполняют, игрок не отдаёт приказ «сесть». В панели локации показываем
|
||||
имена действий у стоящих предметов. Персонажи и «А говорит с Б» — секции панели, списки пустые.
|
||||
Должности берём из 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 создания его нельзя снять. Остальные папки — чекбоксы.
|
||||
|
||||
Живой образец — [`../../src/HSchool.Server/mods/example/`](../../src/HSchool.Server/mods/example/):
|
||||
`pack.jsonc`, две черты, набор имён, патч, last-wins и одна комната. Пока его не выбрали, ваниль
|
||||
не меняется. README внутри папки — как добавить своё.
|
||||
|
||||
```
|
||||
mods/
|
||||
core/
|
||||
defs/ # JSONC типов
|
||||
localizations/
|
||||
ru.jsonc
|
||||
en.jsonc
|
||||
maps/
|
||||
default.jsonc # ванильная раскладка
|
||||
example/ # образец, не контент; выключается в create
|
||||
defs/
|
||||
patches/ # правки чужих def, не копии
|
||||
localizations/
|
||||
ru.jsonc
|
||||
en.jsonc
|
||||
romance/ # контентный пак: ориентация, симпатия, пары 18+, темы
|
||||
defs/orientations/
|
||||
defs/affinity/
|
||||
defs/traits/
|
||||
defs/topics/
|
||||
patches/
|
||||
localizations/
|
||||
```
|
||||
|
||||
Порядок загрузки: сначала `core`, потом выбранные моды в том порядке, в каком они стоят в
|
||||
запросе create. Одинаковый `defName` или ключ локали — **побеждает последний**. Имеет смысл
|
||||
писать предупреждение в лог, не молча, чтобы конфликт модов было видно.
|
||||
|
||||
Ключи локализации — по `defName` (и суффиксам вроде `.label`, если понадобятся несколько строк
|
||||
на тип). Нет ключа — в UI показывается `defName`, это сразу видно в тесте мода.
|
||||
|
||||
Хром клиента (меню, кнопки) по-прежнему в `src/HSchool.Client/src/i18n/`. Строки содержания
|
||||
сервер резолвит из `localizations/` включённых модов под язык клиента и кладёт в снимок / в
|
||||
ответ каталога для редактора. Браузер файлы модов не читает.
|
||||
|
||||
В диалоге создания показывают `core` как включённый и заблокированный плюс список остальных
|
||||
папок. Тело create несёт только *дополнительные* id (сервер всё равно подставит `core` первым).
|
||||
Работник школы грузит этот набор и замораживает каталог. Новые файлы на диске влияют только на
|
||||
следующие школы; список модов для диалога можно сканировать при `GET`, без рестарта процесса.
|
||||
|
||||
## Где это живёт в решении
|
||||
|
||||
- Каталог def, раскладка и валидация — **HSchool.Content**, см. [`projects.md`](projects.md).
|
||||
`HSchool.Simulation` только пользуется уже собранным каталогом. Пути `mods/` и `saves/` — Server.
|
||||
- Инстанс карты (граф) — тип из Content, лежит в `School` (Simulation) вместе с часами и ECS.
|
||||
Поток и файл сейва — Server, см. [`runtime.md`](runtime.md) и [`projects.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 |
|
||||
| Образец пака | `mods/example/`: настоящая папка рядом с `core`, не контент |
|
||||
| Коллизии имён | Последний пакет победил (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`](runtime.md) |
|
||||
| Код модов (DLL) | Не в этом срезе |
|
||||
Reference in New Issue
Block a user