Решили: TapeChunkWriter.Write стал TryWrite(tape, firstTick, tickCount, maxBytes, out bytes).
Предел приходит параметром, false означает ровно «не влезло», номер чанка тратится только на
успешной записи. DiscardLast удалён за ненадобностью.
Почему: пределов было два — потолок формата (64 КБ, у писателя) и предел транспорта (у Steam
512 КБ, у стримера), — и реагировали они по-разному: писатель бросал исключение, стример делил
диапазон пополам. Пока чанк оставался меньше 64 КБ, работало деление по пределу транспорта; чанк
между 64 КБ и 512 КБ ронял исключение, не дав стримеру ничего решить. Отвергнутые альтернативы:
поднять MaxChunkBytes до предела транспорта (одна строка, но остаются и throw как поток
управления, и DiscardLast, и сгорающий номер) и «признать деление недостижимым» (сохраняет
зависание раздачи).
Опора не только на вкус: конвенция .NET для TryFormat/TryWrite формулирует то же правило дословно
— возвращать false следует ТОЛЬКО при нехватке места в буфере, любые другие сбои бросают, а
вызывающий на false меняет размер и повторяет. У нас «повторить» — это поделить диапазон тиков.
Грабли: номер чанка увеличивался ДО проверки размера, поэтому исключение уносило его с собой:
_nextTick не двигался, DiscardLast не вызывался, и каждый следующий кадр повторял то же
исключение. Раздача вставала навсегда, а у гостя оставалась дыра в нумерации, которую он просил
повторить до конца боя. Пока номер тратится раньше успеха, любой отказ ниже по коду означает вечную
дыру — поэтому правильный порядок («сначала решение, потом расход») дороже любой обработки ошибок.
Владелец правды: TapeChunkWriter.TryWrite (контракт в <returns>), TapeStreamer.SendChunk
(вычисление предела), тесты ChunkOverTheLimit_ReturnsFalse_AndKeepsTheChunkNumber и
ChunkTooBigForTheTransport_IsSplit_NotDropped.