Решили: присутствие — склеенный пакет всех курсоров ненадёжным каналом, 128 Гц с dirty-check (PresenceSender), а плавность строится на приёме кривой Эрмита по паре «точка + скорость» (PresenceInterpolator).

Почему скорость едет в пакете. Не ради механики — ради интерполяции. Линейная между точками даёт угол на каждом изломе траектории, а курсор в коопе только из изломов и состоит. С касательными кривая совпадает на стыке не только положением, но и направлением, и угол исчезает. Это то же решение, что у Фидлера в snapshot interpolation, и оно объясняет цифру 128: частота отправки не покупает плавность, она покупает размер буфера, а через него — задержку.

Экстраполяция ограничена 100 мс. Потерянный пакет продолжается по последней скорости, но недолго: курсор, улетевший по инерции, читается как баг, а замерший — как «человек отошёл». Второе честнее, потому что за сто миллисекунд рука успевает передумать.

Инвариант «присутствие не трогает игру» держат сборки, а не дисциплина: Guildmaster.Net не ссылается на Guildmaster.Guild, поэтому дороги из курсора в RunState физически нет. Как только от жеста начнёт что-то зависеть (перехват вещи, кубик), он перестанет быть присутствием и станет командой с номером и подтверждением — и тогда переезд заметит компилятор.

Грабли:

  • Номер пакета — ushort и идёт по кругу. Наивное сравнение «новее = больше» на 65536-м пакете сделало бы все следующие «старыми», и чужой курсор замер бы навсегда. Сравнивается разность, приведённая к short.
  • Тест на Эрмита сначала смотрел в середину отрезка — и был неправ. При одинаковых касательных на обоих концах середина это точка перегиба: кривая ровно пересекает прямую, и проверка прошла бы даже для линейной интерполяции, то есть не проверяла бы ничего. Смотреть надо на четвертях, где видно, что кривая ушла в сторону скорости и вернулась.
  • Пакет обязан влезать в MTU: фрагментированное ненадёжное сообщение теряется целиком, и «потерять всех разом» хуже, чем не отправить одного. Отсюда потолок числа курсоров в пакете.

Не сделано намеренно: отрисовка курсора на экране. Это UI-слой и сцена; модель, кодек, отправитель и интерполятор к ней готовы и от неё не зависят — курсорный слой рисуется поверх и разметки экранов не ждёт.

Владелец правды: Net/Presence/ целиком; инварианты — PresenceTests (курсор в покое не стоит ни пакета, потолок частоты, скорость меряется между замерами, роундтрип склейки, отказ на чужой версии, интерполяция уважает направление, инерция с потолком, опоздавший пакет не откатывает курсор, переполнение номера).