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:
Leonid Pershin
2026-08-20 12:26:13 +03:00
parent 12cc36b4dd
commit a37a5eb82d
101 changed files with 1015 additions and 921 deletions
+250
View File
@@ -0,0 +1,250 @@
# Сессия и темп
Договорённость на следующий срез, не текущий код.
Оболочка: [`near-term.md`](../01-shell/near-term.md). Рантайм: [`runtime.md`](../01-shell/runtime.md).
Протокол: [`../protocol.md`](../../protocol.md).
Один процесс сервера, несколько браузеров в локальной сети. Это не аккаунты и не интернет:
общий пароль альфы пускает к двери, имя отличает игроков. Школу по-прежнему тикает её работник,
кто бы ни смотрел. Параллельно часы становятся смотрибельными: ×1 — минута за секунду, высокие
скорости не гоняют тяжёлые системы двадцать раз в секунду.
Срез отвечает на вопросы «кто я», «чьи это школы» и «насколько быстро идёт календарь». Оценок,
приказов и облачных аккаунтов нет.
## Что меняется в уже принятом
| Было | Стало | Почему |
| --- | --- | --- |
| Любой, кто открыл клиент, видит и трогает все школы | Сначала пароль, потом имя; свои школы и чужие — разные списки | Иначе локальный мультиплеер — это гонка за одну кучу сейвов |
| `MaxSchools` = 6 на весь процесс | `MaxSchools` = 2 **на игрока**; отдельно `MaxSchoolsTotal` на процесс | Четверо по две школы уже не влезают в шесть воркеров |
| ×½ ×1 ×2 ×3 ×4, ×1 = 5 игровых минут/с | ×½ ×1 ×2 ×5 ×10, ×1 = 1 игровая минута/с | Пять минут в секунду нельзя смотреть; ×5/×10 нужнее, чем ×3/×4 |
| Скорость только множит минуты, тик всегда 20 Гц | Календарь по-прежнему 20 Гц; ходьба, решения и нужды на ×5/×10 реже | Дорогое — люди, не `GameClock.Advance` |
## Этап A — кто вошёл
### Пароль альфы
Один пароль на сервер, в конфиге `HSchool:AlphaPassword`. Это не пароль пользователя и не хеш
в базе: кто знает строку — может назваться любым ещё не занятым в сети именем.
Пустой пароль в конфиге — сервер не стартует (валидация опций). В репозитории для локальной
игры стоит заглушка `alpha`; хостовые тесты задают своё через окружение AppHost, как уже задают
папку сейвов.
### Имя
После верного пароля клиент просит имя. Нормализация та же, что у школы: обрезка, без
управляющих, 1–40 символов. Занятость **без учёта регистра**: `Leo` и `leo` — одно имя. Как
написали в первый раз — так и показывается.
Имя **закреплено навсегда** в `saves/users.json` (рядом со школами, тот же каталог). С тем же
паролем альфы в него можно войти снова. «Занято» (`409` `name-online`) — только если сейчас
есть живой WebSocket с этим именем. Закрыл вкладку — имя снова свободно для входа, в том числе
с другой машины. Две вкладки одного браузера: второе соединение получает «занято».
Это сознательная слабость альфы: пароль общий, имя и есть личность. Кто знает и пароль, и имя,
может войти как этот человек, пока тот офлайн.
### Куки
`POST /api/session` `{ "password", "userName" }` ставит **HttpOnly**-куку сессии
(`SameSite=Lax`, `Path=/`). Клиент пароль в `localStorage` не кладёт. Срок — недели, чтобы не
вводить каждый запуск; точное число — поле конфига, не константа в коде.
Кука — подписанный идентификатор (ASP.NET Data Protection или HMAC), не «голый» пароль.
Рестарт процесса не выкидывает всех: подпись проверяется заново. Живое «онлайн» при рестарте
сбрасывается — сокеты умерли.
`GET /api/session` — текущее имя или `401`. `DELETE /api/session` — выйти, сменить имя.
Без выхода второе имя с той же куки не взять.
WebSocket `/ws/game` без валидной куки не получает Welcome: закрытие с политикой, не кадр.
Hello по-прежнему несёт только версию протокола и локаль — имя с куки, не с кадра. Раскладка
сокета не меняется, версия протокола не растёт.
Клиент **сначала** сессия, **потом** сокет. Сейчас сокет открывается вместе со страницей;
после этого среза — только когда кука уже есть. Реконнект сокета несёт ту же куку сам.
### Что закрыто без сессии
Все `/api/schools`, каталог, моды, люди, расписание, найм — `401`. Живут без куки:
- `GET /health`
- `POST` / `GET` / `DELETE /api/session`
- дев-ручки под `HSchool:AllowSaveReload`**с** кукой, как остальной API
Хостовые тесты логинятся в `ResetAsync` (или рядом): один тестовый пользователь на прогон,
иначе каждый тест упирается в `401`.
## Этап B — чьи школы
### Потолки
`SimulationOptions.MaxSchools` меняет смысл: это **слоты одного игрока**, по умолчанию 2.
Старое «шесть на процесс» уезжает в `MaxSchoolsTotal` (по умолчанию 16) — сколько воркеров
вообще можно поднять.
Create отказывает:
- своих уже `MaxSchools``limit-reached` (как сейчас, но считает только школы этого имени);
- воркеров уже `MaxSchoolsTotal` — тот же код или соседний `server-full`. Два кода лучше:
игрок понимает, кончились *его* слоты или весь сервер.
При старте процесса с диска поднимаем до `MaxSchoolsTotal` файлов, не до `MaxSchools`. Иначе
четверо игроков по две школы потеряют сейвы после рестарта.
`GET /api/schools` и Welcome по-прежнему несут `maxSchools` — теперь это слоты игрока. Байтов
Welcome не трогаем, версию не бампим. Глобальный потолок — только в HTTP (`maxSchoolsTotal`).
### Хозяин
На сейве поле `owner` — нормализованное имя. Формат не бампим: поле опциональное, лишние JSON
поля и так игнорируются. Нет поля / пусто — школа **бесхозная**.
Бесхозные и чужие живут в одном втором списке меню. Бесхозную может **удалить любой**
залогиненный (почистить альфу после обновления). Смотреть — все. Пауза, скорость, наём,
правила — никто: хозяина нет. Забрать себе бесхозную нельзя.
Новая школа записывает хозяина в том же create, что и имя.
### Меню
Два блока, не два браузерных окна:
1. **Мои** — нынешние карточки: создать, открыть, удалить. Кнопка создания гаснет по своим
слотам, не по чужим.
2. **Чужие** — карточка с именем хозяина (или «—», если бесхозная), открыть; удалить только
у бесхозной.
Опрос раз в секунду тот же: патч карточек на месте, не пересборка сетки.
### Гость внутри школы
Открыть чужую можно. Сервер пускает `OpenSchool` любому с сессией. Кадры часов и присутствия
идут как хозяину: школа и так тикает без зрителей.
Гость **не** может:
- пауза, скорость, пропуск пустого времени — кадр сокета игнорируется (не закрывает соединение);
- удалить, нанять, назначить, сменить правила, закрепить урок — HTTP `403` `not-owner`;
- генерировать портреты — **можно**: пресеты лежат на школе, модель одна и та же;
- видеть вкладку «Управление» — клиент её не монтирует. Карта и люди — да, карточка — да.
Хозяин, который смотрит свою, ничего не теряет. Несколько гостей на одну школу — несколько
подписок на те же кадры.
Клиент не угадывает права: `GET /api/schools/{id}` (карточка меню или мелкая сводка при
открытии) несёт `owner` и `mine`. Кнопки часов и вкладка управления — от `mine`, не от
«я залогинен».
## Этап C — темп
База `GameMinutesPerRealSecond = 1`. Таблица кнопок, индекс на проводе как сейчас:
| Индекс | Подпись | Множитель |
| --- | --- | --- |
| 0 | ×½ | 0.5 |
| 1 | ×1 | 1 |
| 2 | ×2 | 2 |
| 3 | ×5 | 5 |
| 4 | ×10 | 10 |
×1 по-прежнему стартовый индекс. ×3 и ×4 уходят: индексы 3 и 4 — это теперь ×5 и ×10.
Раскладка `SetSpeed` не меняется (байт индекса), версия протокола не растёт. Зеркало таблицы
в `ClockSpeed.cs` и `protocol.ts` правится вместе, как сейчас.
Инвариант фиксированного шага жив: воркер по-прежнему будит мир 20 раз в секунду и двигает
календарь на каждом импульсе. Нельзя заменить это «прыжком на десять минут одним Tick» — тогда
нужды и ходьба применятся в новой точке, а не по пути.
На ×½, ×1 и ×2 тяжёлые системы вызываются каждый импульс, как сейчас.
На ×5 и ×10 тяжёлые системы вызываются реже, с **накопленными** игровыми минутами с прошлого
вызова:
| Скорость | Импульс календаря | Тяжёлые системы | Накопление |
| --- | --- | --- | --- |
| ×5 | каждый (20 Гц) | каждый 4-й | 1 игровая минута |
| ×10 | каждый (20 Гц) | каждый 2-й | 1 игровая минута |
Так на высоких скоростях квант ходьбы и нужд — одна игровая минута, а не двадцать мелких шагов
в секунду. Экран часов обновляется с 20 Гц: минуты сами крупные, кадр не прыгает через час.
Тяжёлые (реже):
- ходьба и присутствие (`PresenceSystem.Apply`);
- действия и очередь решений;
- декей нужд и тепла;
- рост навыка на уроке;
- износ одежды.
Каждый импульс, даже на ×10:
- `GameClock.Advance`;
- граница суток лога (6:00);
- годовой набор и недельное обновление пула;
- погода, когда она и так пересчитывается по дате.
Пауза по-прежнему не двигает календарь; пропуск пустого времени — по-прежнему прыжок, не тик.
Потолок догона воркера (5 шагов, лишнее сбросить) не трогаем.
## Поток данных
Имя живёт на соединении после handshake, не в каждом HTTP-теле. HTTP читает куку, сокет —
ту же куку на upgrade. Супервизор знает `userName` клиента и при create/delete/командах часов
сверяет с `owner` школы. Работник по-прежнему не знает про «пользователя»: `SetRunning` либо
доходит из супервизора, либо нет.
`users.json` пишет супервизор (или тонкий сервис рядом), не работник школы: это не мир.
Школьный `owner` пишет работник при create/persist, как остальные поля сейва.
## Где живёт код
Нового проекта нет.
| Что | Куда |
| --- | --- |
| Пароль, кука, `users.json`, онлайн по имени | `HSchool.Server` |
| `owner` на сейве, потолки create | `HSchool.Server` + поле на снимке |
| HTTP сессии, 401/403, два списка меню | `HSchool.Server/Api` + клиент + `docs/protocol.md` |
| Таблица скоростей | `ClockSpeed` + `protocol.ts` (зеркало, не раскладка) |
| База минут/с | `SimulationOptions.GameMinutesPerRealSecond` |
| Страйд тяжёлых систем | `HSchool.Simulation` (`School.Tick`) |
## Советы, которые стоит принять сразу
- Не делать пароль на каждого. Для альфы это второй пикер и «я забыл», без выигрыша.
- Не класть пароль в куку и не слать его в Hello.
- Не бампить протокол из-за смены множителей и смысла `maxSchools`: байты те же.
- Не урезать календарь до 2 Гц на ×10 — тогда часы в UI лгут о «живом» времени.
- Не применять 5 игровых минут одним `PresenceSystem.Apply`: уже написано, чем это кончается.
- Не прятать чужие школы за «добавить в друзья». В альфе все на одном сервере — все видны.
## Заведомо не сейчас
- Регистрация, почта, уникальный пароль на игрока, OAuth.
- Права «редактор» / «админ», передача школы другому.
- Гость крутит часы или видит «Управление».
- Забрать бесхозную себе.
- Облако, отдельный процесс на игрока, синхронизация сейвов.
- Оценки, приказы, вызов родителей.
- Перегруз ходьбы от ноши, штраф урока отдельным правилом — рост навыка уже режется нуждой.
## Зафиксировано этим разговором
| Тема | Решение |
| --- | --- |
| Пароль | Один, `HSchool:AlphaPassword`; пустой — не стартовать |
| Имя | Закреплено навсегда; «занято» = живой сокет; регистр не различает |
| Куки | HttpOnly-сессия, не пароль в `localStorage` |
| Сокет | После сессии; без куки нет Welcome; имя не в кадре |
| Слоты | `MaxSchools` на игрока (2); `MaxSchoolsTotal` на процесс (16) |
| Меню | Мои / чужие; бесхозные в чужих, удалить может любой |
| Гость | Смотрит карту и людей; часов не трогает; «Управление» скрыто |
| Хозяин | Поле `owner` на сейве; нет поля — бесхозная |
| ×1 | 1 игровая минута за реальную секунду |
| Кнопки | ×½ ×1 ×2 ×5 ×10 |
| Тики | Календарь 20 Гц всегда; тяжёлые системы на ×5/×10 с шагом 1 игровая минута |
| Протокол | Раскладка не меняется |
| Игрок гостя | Наблюдает |