Files
h-school/docs/design/08-session/session.md
T

18 KiB
Raw Blame History

Сессия и темп

Договорённость на следующий срез, не текущий код. Оболочка: near-term.md. Рантайм: runtime.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 отказывает:

  • своих уже MaxSchoolslimit-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 игровая минута
Протокол Раскладка не меняется
Игрок гостя Наблюдает