Give packs an identity so create can refuse missing deps and load in a stable order.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 00:20:36 +03:00
co-authored by Cursor
parent e5a0ae5b9d
commit 263c94c55d
28 changed files with 851 additions and 56 deletions
+8 -9
View File
@@ -9,12 +9,11 @@
## Почему сейчас
**Моды существуют на бумаге.** В `mods/` лежит один `core`. Last-wins, патчи и порядок загрузки
проверяются синтетическими документами в памяти, а путь «игрок выбрал мод» не проверяется вообще —
подставить нечего. Пак не имеет удостоверения: `GET /api/mods` отдаёт `{id, required}`, и в диалоге
создания игрок видит имя папки. Зависимостей между паками нет, поэтому мебельный набор, который
патчит чужой def, может быть выбран без того, кого он патчит, — и школа не соберётся с невнятной
ошибкой каталога.
**Моды существуют на бумаге — остался проверяемый путь.** Удостоверение пака уже есть:
`pack.jsonc` (версия и `requires`), название в локалях по id, `GET /api/mods?lang=` отдаёт
подпись, создание отказывает во внятном коде, если зависимости нет или они замкнуты в цикл.
Порядок загрузки сервер выстраивает сам и кладёт в сейв. В `mods/` по-прежнему лежит один
`core`: last-wins, патчи и дорога «игрок выбрал мод» ждут настоящую папку — это фаза 23.
**Числа поведения живут в коде вопреки [`ai.md`](ai.md).** Там записано: «Числа поведения —
отдельный деф правил, как `StaffingDef` у штата». В `BehaviorDef` уехали порог нужды, скорость
@@ -100,9 +99,9 @@
нанимает второго, ничего не меняется, и понять почему неоткуда. В строку непокрытого предмета
добавляется, сколько человек его не вытягивают.
**Def без подписи.** Загрузчик молча подставляет `defName`, когда ключа локали нет. Для `core` это
ловит тест на полноту, для мода — ничего. Загрузчик начинает писать предупреждение в лог: мод-автор
видит дыру сразу, а не по кривой подписи в дереве.
**Def без подписи.** Загрузчик пишет предупреждение в лог, когда у конкретного неабстрактного def
нет ключа ни в `ru`, ни в `en`. Для `core` это ловит тест на полноту; для мода автор видит дыру
сразу, а не по кривой подписи в дереве. Каталог при этом собирается — подставляется `defName`.
## Сид школы — свой, а не производный
+16 -16
View File
@@ -11,31 +11,31 @@
## Задачи
- [ ] `pack.jsonc` в папке пака: `version` строкой, `requires` списком id. Файла нет — пак
- [x] `pack.jsonc` в папке пака: `version` строкой, `requires` списком id. Файла нет — пак
по-прежнему валиден: id вместо названия, версия пустая, зависимостей нет
- [ ] Название пака — ключ по его id в его же `localizations/<lang>.jsonc`; второго способа
- [x] Название пака — ключ по его id в его же `localizations/<lang>.jsonc`; второго способа
называть вещи не заводить
- [ ] `GET /api/mods` принимает `?lang=ru|en` и отдаёт `label`, `version` и `requires` рядом с
- [x] `GET /api/mods` принимает `?lang=ru|en` и отдаёт `label`, `version` и `requires` рядом с
`id` и `required`
- [ ] У `core` такой же `pack.jsonc` и такое же название в локалях
- [ ] Создание школы проверяет, что каждая зависимость выбрана; нет — `400` с кодом и id того,
- [x] У `core` такой же `pack.jsonc` и такое же название в локалях
- [x] Создание школы проверяет, что каждая зависимость выбрана; нет — `400` с кодом и id того,
кого не хватает
- [ ] Порядок загрузки выстраивает сервер: устойчивая топологическая сортировка поверх порядка
- [x] Порядок загрузки выстраивает сервер: устойчивая топологическая сортировка поверх порядка
игрока, `core` всегда первый. Цикл зависимостей — отказ
- [ ] Разрешённый порядок виден: пишется в лог при старте школы и возвращается в ответе создания
- [ ] Сейв хранит **разрешённый** порядок паков, чтобы школа поднималась тем же каталогом
- [ ] Загрузчик пишет предупреждение, когда у конкретного def нет подписи в локали пака
- [ ] `docs/protocol.md` и [`../design/foundation.md`](../design/foundation.md) правятся тем же
- [x] Разрешённый порядок виден: пишется в лог при старте школы и возвращается в ответе создания
- [x] Сейв хранит **разрешённый** порядок паков, чтобы школа поднималась тем же каталогом
- [x] Загрузчик пишет предупреждение, когда у конкретного def нет подписи в локали пака
- [x] `docs/protocol.md` и [`../design/foundation.md`](../design/foundation.md) правятся тем же
коммитом, что и обработчики
## Тесты, без которых фаза не закрыта
- [ ] Пак без `pack.jsonc` виден в списке, id стоит вместо названия
- [ ] Название приходит на языке запроса, у `core` тоже
- [ ] Пак с невыбранной зависимостью не создаёт школу; в ответе видно, кого не хватает
- [ ] Зависимость, выбранная после зависимого, всё равно грузится раньше
- [ ] Цикл зависимостей — отказ, а не зависание
- [ ] Def без подписи даёт предупреждение, но не роняет каталог
- [x] Пак без `pack.jsonc` виден в списке, id стоит вместо названия
- [x] Название приходит на языке запроса, у `core` тоже
- [x] Пак с невыбранной зависимостью не создаёт школу; в ответе видно, кого не хватает
- [x] Зависимость, выбранная после зависимого, всё равно грузится раньше
- [x] Цикл зависимостей — отказ, а не зависание
- [x] Def без подписи даёт предупреждение, но не роняет каталог
## Критерий готовности
+19 -8
View File
@@ -40,7 +40,7 @@ from Monday (five is MonFri; six adds Saturday). It is a school rule, not a c
"gameMinutesPerRealSecond": 5,
"schoolWeekDays": 5,
"schools": [
{ "id": 1, "name": "Гимназия №14", "gameTime": "2012-03-31T07:35:00Z", "running": false, "speedIndex": 1 }
{ "id": 1, "name": "Гимназия №14", "gameTime": "2012-03-31T07:35:00Z", "running": false, "speedIndex": 1, "modIds": ["core"] }
]
}
```
@@ -53,20 +53,27 @@ Optional `?lang=en` draws from the English word list (`Northern Academy`); any o
none, stays Russian. The client sends the active UI language. Names the player types are not
translated — they are saved as written.
### `GET /api/mods`
### `GET /api/mods?lang=ru|en`
Folders under the server's `mods/` directory. `core` is always first and `required: true`; other
packs can be switched off in the create dialog.
packs can be switched off in the create dialog. `lang` is the same value Hello carries — not
`Accept-Language`. Anything other than `en` is Russian.
Each pack carries a human label, a version string and the ids it `requires`. The label is the pack
id looked up in that pack's own `localizations/<lang>.jsonc`. A folder without `pack.jsonc` is
still a pack: the id stands in for the name, `version` is empty, `requires` is empty.
```json
{ "mods": [{ "id": "core", "required": true }] }
{ "mods": [{ "id": "core", "required": true, "label": "Базовая игра", "version": "1.0", "requires": [] }] }
```
### `GET /api/catalog?lang=ru|en&mods=addon1,addon2`
Placeable (non-abstract) types plus labels in `lang`, and the last-wins `maps/default.jsonc` for
`core` plus the listed extras. The server always prepends `core`. `mods` is a comma-separated
list of extra pack ids; omit it for vanilla. Unknown extras return `400` `unknown-mod`.
`core` plus the listed extras. The server always prepends `core` and then reorders extras so
`requires` load first, same as create. `mods` is a comma-separated list of extra pack ids; omit
it for vanilla. Unknown extras return `400` `unknown-mod`. A selected pack whose dependency was
not listed returns `400` `missing-mod`; a cycle returns `400` `mod-cycle`.
Room defs that are homerooms carry `homeroom`, `seatThing` and `defaultSeats` instead of a
slot table. A map classroom stores `seats` — how many of that thing occupy the room. Capacity is
@@ -114,8 +121,10 @@ Body:
}
```
`modIds` are extras; the server always prepends `core`. Omit `map` (or send `null`) to use that
pack set's default layout. A supplied map is validated as a connected yard-and-rooms graph.
`modIds` are extras; the server always prepends `core`, then **reorders** the selection so each
pack's `requires` load first (stable topological sort over the player's order). The resolved
order is returned as `modIds` on the created school and written to the save, so a restart loads
the same catalog. Omit `map` (or send `null`) to use that pack set's default layout. A supplied map is validated as a connected yard-and-rooms graph.
`nameSetId` is a `NameSetDef`; omit it to use the first placeable set in the catalog (vanilla:
`Slavic`). Unknown ids return `400` `unknown-name-set`.
`nativeLanguage` is a skill from that set's `nativeLanguages`. Omit it (or send `null`) to pick
@@ -128,6 +137,8 @@ one from the school seed. An id that is not in the set returns `400` `unknown-na
| `400` `invalid-start-date` | Outside 19002999. |
| `400` `invalid-map` | Missing yard, no rooms, unknown def, or a disconnected graph. |
| `400` `unknown-mod` | An extra pack id is missing under `mods/`. |
| `400` `missing-mod` | A selected pack `requires` an id that was not selected. `missing` is that id. |
| `400` `mod-cycle` | Selected packs require each other in a cycle. |
| `400` `invalid-catalog` | The selected packs could not be loaded. |
| `400` `unknown-name-set` | `nameSetId` is not a placeable `NameSetDef` in those packs. |
| `400` `unknown-native-language` | `nativeLanguage` is not in that name set's `nativeLanguages`. |