Решили: присутствие режется по сторонам на отправке у хоста, а «кто за какую сторону играет» получает единственного владельца — состав сеанса (ISessionRoster), от которого теперь производен и ILocalPlayer.Team.

Почему: отсечь чужой курсор на отрисовке было бы дешевле на один метод и бесполезно по сути — байты уже лежали бы в памяти клиента противника, а курсор в фазе расстановки это и есть чужой строй. Отвергнутая альтернатива по владельцу: положить сторону в мероприятие («сторон нет в хабе, значит это его свойство»). Не выдержала сверки с кодом — шов «за какую команду играет этот клиент» уже существовал (ILocalPlayer, читают шестеро), жил в корне и пережил бы любое мероприятие; а режим выбирается на входе в игру и внутри сеанса не меняется. У мероприятия остался ровно один вопрос, на который отвечает только оно: сколько сторон у площадки (ActivitySideAssignment).

Сказано: «Курсор надо видеть лишь у членов своей команды. Надо завести нормальный источник правды кто какой команды, если еще нет, и использовтаь его в текущем пвп и в курсорах» (03.08.2026, сессия b307a49b). Тем же ходом выбран вариант «курсоры в бою видны, но приглушены» — прежнее правило кооп-кластера «курсор гаснет в бою» отменено.

Грабли:

  • Источника правды не было вовсе, а выглядело как будто был. «Своя команда» жила литеральным нулём в четырёх местах DeploymentController, и пока за игру садился один человек, ноль был верен по совпадению. В PvP вдвоём оба клиента считали бы своей одну и ту же сторону — то есть PvP был сломан ещё до курсоров. При этом ILocalPlayer с точным докстрингом («в PvP у клиентов разные Team») существовал с самого начала и расстановкой не спрашивался.
  • Не всякий ноль в расстановке означает «я». SideOf(team) — это половина арены, а не сторона смотрящего: игрок за команду 1 обязан расставляться в правой зоне и у себя, и у противника, иначе бойцы у двух клиентов стояли бы в разных местах одной арены. Заменять там ноль нельзя, и это записано прямо в коде — иначе следующий заход поменяет всё подряд.
  • Забывать чужой курсор по давности пакета нельзя. Соблазнительно и неверно: курсор в покое не шлёт пакетов вовсе (в этом весь смысл dirty-check), поэтому «давно молчит» значит «человек не двигает мышью». Ушедший и сменивший сторону забываются по составу сеанса, который приходит надёжным каналом ровно тогда, когда надо забыть.
  • Второй перевод «экран → мир» появился бы молча. Расстановка считала его сама; присутствию нужен тот же ответ. Свели к одному шву IPointerWorld — разъехались бы они не сразу, а на первой правке глубины камеры, и выглядело бы это как «курсор чуть-чуть не там».
  • Мейн-цвет едет по сети местом в наборе, а не значением: владельцем цвета остаётся палитра (токены --gm-color-player-1..4), даже когда цвет прилетел из чужого клиента.

Владелец правды: Core/Players/ISessionRoster.cs, Game/Session/Net/HostPresence.cs, Game/Activity/ActivitySideAssignment.cs, тесты PresenceSidesTests.