Решили: кадр покоя уезжает БЕЗ событий — только снимки. События едут ровно один раз, в том чанке, который двигает бой вперёд.
Почему: чанк несёт и снимки, и события диапазона, а кадр покоя переотправляется десять раз в секунду, пока арена стоит (расстановка, площадка). Приёмник честно применял его каждый раз — и проигрывал удары, смерти и их звуки заново. Слышалось это как зациклившийся звук.
Грабли: мой же регресс, введённый в тот же день вместе с кадром покоя. Симптом не похож на причину совсем: «у клиента дублируется какой-то странный звук, мб UI / SFX» — и искать его хочется в аудио, а лежит он в сетевом кодеке. Связь нашлась только потому, что кадр покоя был свежим подозреваемым.
Правило, которое из этого следует: повторная отправка чего угодно обязана быть идемпотентной у приёмника. Снимок идемпотентен по построению — он перезаписывает слот кольца по своему тику. Событие не идемпотентно никогда: его применение — это действие, а не состояние.
Владелец правды: Net/Tape/TapeChunkCodec.cs (TryWrite(..., includeEvents)),
Net/Tape/TapeChunkPump.cs (Resend).