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,172 @@
|
||||
# Фундамент: паки, числа, сид, версия и золотые файлы
|
||||
|
||||
Договорённость на срез после жизни школы. Срез закрыт фазами 22–28; таблица внизу — решения.
|
||||
Абзацы «почему сейчас» ниже описывают дыры, которые срез закрыл, а не текущий код.
|
||||
|
||||
## Почему сейчас
|
||||
|
||||
**Моды существуют на бумаге — остался проверяемый путь.** Удостоверение пака уже есть:
|
||||
`pack.jsonc` (версия и `requires`), название в локалях по id, `GET /api/mods?lang=` отдаёт
|
||||
подпись, создание отказывает во внятном коде, если зависимости нет или они замкнуты в цикл.
|
||||
Порядок загрузки сервер выстраивает сам и кладёт в сейв. Рядом с `core` лежит `mods/example` —
|
||||
маленький скучный пак на диске, чтобы last-wins, патчи и дорога «игрок выбрал мод» проверялись
|
||||
настоящей папкой, а не документами в памяти.
|
||||
|
||||
**Числа поведения уехали в `BehaviorDef`.** Порог нужды, обучение, разброс на дорогу и веса целей
|
||||
(урок, переход, нужда на нуле, обед) — данные. Каталог без дефа падает на константы кода, а не
|
||||
ломается. Пак может поднять `lunchWeight` выше урока без форка.
|
||||
|
||||
**Золотые файлы ловят то, что раньше ловили глазами.** Отпечаток ростера — файл в фикстурах.
|
||||
Сейв текущего формата и сейв без `nativeLanguage` грузятся тестом хоста. Формат новее своего
|
||||
школу не стартует и файл не трогает.
|
||||
|
||||
## Удостоверение пака
|
||||
|
||||
`mods/<id>/pack.jsonc` — один объект, не список:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"version": "1.0",
|
||||
"requires": ["core"],
|
||||
}
|
||||
```
|
||||
|
||||
**Название — не в этом файле.** Оно живёт там же, где все остальные подписи: в
|
||||
`localizations/<lang>.jsonc` того же пака, ключом по id пака. Заводить второй способ называть вещи
|
||||
ради одной строки не стоит, а так мод-автор пишет название рядом с названиями своих комнат.
|
||||
|
||||
**Файла может не быть.** Папка без `pack.jsonc` — по-прежнему валидный пак: id вместо названия,
|
||||
версия пустая, зависимостей нет. Быстрый мод «две черты и набор имён» не должен требовать церемоний.
|
||||
|
||||
**`requires` — присутствие и порядок.** Пак, который патчит чужие def, обязан грузиться после
|
||||
того, кого патчит. Поэтому сервер не просто проверяет, что зависимость выбрана, а **переставляет**
|
||||
выбранные паки так, чтобы зависимости шли раньше (устойчивая топологическая сортировка: порядок
|
||||
игрока сохраняется всюду, где он не противоречит зависимостям). Тихой перестановка не будет —
|
||||
разрешённый порядок возвращается в ответе создания и пишется в лог.
|
||||
|
||||
Отсутствующая зависимость — отказ создания с внятным кодом, а не сломанный каталог. Цикл
|
||||
зависимостей — тоже отказ: сортировать нечего.
|
||||
|
||||
**`core` не особенный.** У него такой же `pack.jsonc` и такое же название в локалях; особенность у
|
||||
него ровно одна и прежняя — его нельзя снять.
|
||||
|
||||
## Пример пака в репозитории
|
||||
|
||||
Один настоящий пак рядом с `core`, маленький и скучный: пара черт, набор имён, патч чужого def и
|
||||
своя строка локализации. Он нужен не как контент, а как **проверяемый путь**: каталог с модом,
|
||||
создание школы с модом, патч поверх `core`, конфликт `defName` и last-wins, зависимость. Сегодня
|
||||
всё это живёт только в тестах с документами в памяти, а дорога от папки на диске до школы не
|
||||
проверена ни разу.
|
||||
|
||||
Пак должен быть виден из тестов хоста, то есть лежать рядом с `core` в `mods/` и ехать в вывод
|
||||
сборки теми же правилами. Живая папка — `src/HSchool.Server/mods/example/`.
|
||||
|
||||
## Числа поведения — в `BehaviorDef`
|
||||
|
||||
Веса целей переезжают в деф правил: обязанность-урок, обязанность-переход, нужда на нуле, обед.
|
||||
Значения по умолчанию — сегодняшние, чтобы поведение не поменялось ни на минуту. Константы в коде
|
||||
остаются как запасной вариант на случай пака без `BehaviorDef` — каталог без правил поведения
|
||||
должен работать, а не падать.
|
||||
|
||||
Это не рефакторинг ради красоты: после переезда мод, меняющий одно число, перестаёт быть форком.
|
||||
|
||||
## Золотые файлы
|
||||
|
||||
**Ростер.** Отпечаток генерации для зафиксированного сида, карты, набора имён и родного языка —
|
||||
файлом в фикстурах, а не строкой в тесте. Тест сравнивает и, когда расходится, печатает первую
|
||||
несовпавшую строку.
|
||||
|
||||
Правило, без которого это превратится в раздражение: **отпечаток обновляется в том же коммите,
|
||||
что и изменение генерации, и коммит объясняет, что именно поменялось**. Красный золотой тест — это
|
||||
вопрос «ты правда хотел?», а не приказ.
|
||||
|
||||
**Сейв.** Файл школы текущего формата кладётся в фикстуры и живёт там навсегда. Тест грузит его и
|
||||
проверяет, что школа поднялась. Каждое следующее поле сейва обязано оставить этот файл рабочим —
|
||||
именно так проверяется обещание «лишние поля игнорируются, отсутствующие имеют разумное значение».
|
||||
Меняется формат по-настоящему — рядом кладётся новый файл, старый остаётся.
|
||||
|
||||
## Мелкие долги, которые дешевле закрыть здесь
|
||||
|
||||
**Сколько учителей не хватает.** Сейчас `uncovered` говорит «предмет непокрыт». Замер показал, что
|
||||
`PrimarySchool` требует троих: восемьдесят часов в неделю против потолка в тридцать шесть. Игрок
|
||||
нанимает второго, ничего не меняется, и понять почему неоткуда. В строку непокрытого предмета
|
||||
добавляется, сколько человек его не вытягивают.
|
||||
|
||||
**Def без подписи.** Загрузчик пишет предупреждение в лог, когда у конкретного неабстрактного def
|
||||
нет ключа ни в `ru`, ни в `en`. Для `core` это ловит тест на полноту; для мода автор видит дыру
|
||||
сразу, а не по кривой подписи в дереве. Каталог при этом собирается — подставляется `defName`.
|
||||
|
||||
## Сид школы — свой, а не производный
|
||||
|
||||
Сегодня сид генерации это `school.Id`. Отсюда три неприятности сразу: у двух игроков школа №1
|
||||
населена одинаково; состав школы зависит от того, сколько школ создали до неё; воспроизвести чужой
|
||||
баг нельзя, не повторив последовательность идентификаторов. Ревью споткнулось об это дважды — тест,
|
||||
зелёный на машине разработчика и красный на чистом клоне, был ровно про это.
|
||||
|
||||
Сид становится собственным полем: случайный при создании, необязательно передаваемый в запросе,
|
||||
видимый игроку. Файл людей его уже хранит, поэтому существующие школы не меняются — они просто
|
||||
продолжают жить со своим прежним числом.
|
||||
|
||||
Побочная выгода важнее исходной причины: сидом можно поделиться. «Вот моя школа» становится одной
|
||||
строкой, а не архивом сейвов.
|
||||
|
||||
## Версия сейва читается
|
||||
|
||||
`Format` штампуется при записи, копируется при чтении и **никогда не сравнивается**. Файл из
|
||||
будущей версии загрузится молча и будет понят неправильно, а места для миграции не предусмотрено.
|
||||
|
||||
Правило то же, что с пропавшей папкой мода: формат новее своего — школу не стартовать, файл не
|
||||
трогать, в лог. Формат старее — явный шов для апгрейда, сегодня пустой, но названный. Пустой
|
||||
именованный шов дешевле, чем попытка вспомнить через полгода, куда его вставлять.
|
||||
|
||||
## Взгляд внутрь школы
|
||||
|
||||
Всё ревью замеры делались временными тестами: узнать, кто где стоит и с какими нуждами, иначе
|
||||
нечем. Дев-ручка отдаёт ростер, присутствие, расписание и нужды одним JSON — за тем же
|
||||
переключателем, что и перезагрузка сейвов, и никогда включённой по умолчанию.
|
||||
|
||||
Ручка обязана читать опубликованные снимки и ходить через мейлбокс, а не лезть в `World` мимо
|
||||
работника: инструмент диагностики, нарушающий инвариант, сам становится источником загадок.
|
||||
|
||||
## Экраны под тестом
|
||||
|
||||
`AGENTS.md` записывал: у экранов тестов нет, потому что DOM-окружение стоило бы зависимости,
|
||||
которой у проекта нет. На первой фазе это было верно — экран состоял из часов. Сейчас в клиенте
|
||||
несколько тысяч строк, а vitest уже стоит: окружение — одна девзависимость.
|
||||
|
||||
Проверяются не экраны целиком, а места, где на клиенте есть **логика**: сборка запроса из фильтров,
|
||||
превращение кода ошибки в текст, блокировки в диалоге создания. Вёрстка и стили остаются на глаза —
|
||||
их тест всё равно не удержит.
|
||||
|
||||
## Что этот срез не делает
|
||||
|
||||
- Не грузит DLL-моды и не исполняет чужой код. Паки остаются данными.
|
||||
- Не даёт менять набор модов у живой школы. Каталог по-прежнему замерзает на работнике.
|
||||
- Не кладёт `defName` комнаты в снимок карты. Клиент смог бы стилизовать типы помещений от модов,
|
||||
но это версия протокола и правка в трёх местах — не «дёшево», значит не сюда.
|
||||
- Не заводит UI для порядка модов. Порядок — выбор игрока плюс устойчивая топологическая
|
||||
сортировка по `requires`; ручная перестановка ждёт того дня, когда паков станет больше трёх.
|
||||
|
||||
## Зафиксировано этим разговором
|
||||
|
||||
| Тема | Решение |
|
||||
| --- | --- |
|
||||
| Удостоверение пака | `pack.jsonc`: версия и `requires`; название — в локалях по id пака |
|
||||
| Пак без файла | Валиден: id вместо названия, без зависимостей |
|
||||
| Зависимости | Проверяются на присутствие; порядок сервер выстраивает сам |
|
||||
| Порядок загрузки | Устойчивая топологическая сортировка поверх порядка игрока; результат виден в ответе |
|
||||
| Отсутствующая зависимость | Отказ создания с кодом ошибки, не сломанный каталог |
|
||||
| Цикл зависимостей | Отказ |
|
||||
| `core` | Такой же пак; особенность одна — нельзя снять |
|
||||
| Пример мода | Настоящая папка в репозитории, ради проверяемого пути, а не ради контента |
|
||||
| Веса целей | В `BehaviorDef`, значения по умолчанию — сегодняшние |
|
||||
| Каталог без `BehaviorDef` | Работает на константах кода |
|
||||
| Золотой ростер | Файл-отпечаток; обновляется в том же коммите, что и генерация |
|
||||
| Золотой сейв | Файл живёт вечно; новое поле обязано его не сломать |
|
||||
| Непокрытый предмет | Говорит, скольких учителей не хватает |
|
||||
| Def без подписи | Предупреждение в лог при загрузке |
|
||||
| Сид школы | Своё поле: случайный при создании, можно передать, видно игроку |
|
||||
| Старые школы | Продолжают жить со своим прежним сидом из файла людей |
|
||||
| Версия сейва | Читается: новее — не стартуем, старее — именованный шов для апгрейда |
|
||||
| Дев-дамп | За переключателем, через снимки и мейлбокс, не в обход работника |
|
||||
| Тесты экранов | Одна девзависимость; проверяется логика, не вёрстка |
|
||||
Reference in New Issue
Block a user