Skip to main content

ChunkedTransferSession

Tsvrc.DataTransfer.ChunkedTransferSession sequences delivery of a DataChunker-split message: each chunk runs its own ready check (every target player must acknowledge it) before the next chunk starts. It has no networking of its own yet: that's added by DataSender above it. This layer only owns the chunk-by-chunk sequencing state machine. You never call it directly. See DataTransferer for the type a consumer actually uses.

The sequencing loop

TransferData(data, playerIds) stores the data and starts a ready check over the target players. Each completed ready check (OnProcessCompleted) either starts the next chunk's ready check or, on the last chunk, fires OnChunkSequenceCompleted. Four protected virtual hooks mark the transitions a subclass overrides to broadcast lifecycle events: OnChunkSequenceStarted (chunk index 1 about to send), OnChunkSequenceStopped (cancelled, or all targets departed), OnChunkSequenceCompleted (last chunk's ready check completed), and OnDataChunkSendRequested(dataChunk, chunkIndex, totalChunks, playerIds) (fired for every chunk in sequence: this is the actual "send this chunk" signal a subclass hooks to do networking).

If every tracked player leaves or gets suspended while a chunk's ready check is active, the check itself can't auto-complete: ReadyCheckProcess's own completion poll returns early on an empty tracked list rather than treating "nobody left to wait on" as "everyone's ready." ChunkedTransferSession overrides OnProcessUpdate to detect exactly that (an active process with zero tracked players) and calls CancelDataTransfer() itself, so a transfer with no remaining targets doesn't stall forever waiting for a ready check that can never complete on its own.

The inter-chunk gap

Between chunks, the underlying Process is briefly not running: a ReadyCheckProcess's "stopped" state doesn't distinguish "the whole transfer is done" from "this chunk's check finished, the next one hasn't started yet." ChunkedTransferSession tracks that window itself with an internal, synced _pendingNextChunk flag, deferring the actual start of the next ready check to the following frame (SendCustomEventDelayedSeconds(...,0f)) so the previous chunk's tick loop fully exits first. Synced, so a new owner (see below) can detect being mid-gap rather than mistaking it for "no transfer active."

At the start of the next ready check, the target player list is re-filtered against currently active, non-suspended players. OnPlayerLeft/OnPlayerSuspendChanged don't fire this filtering themselves between chunks, since both guard on IsProcessRunning(), which is false during the gap.

Ownership handover mid-transfer

Transfer state (_dataChunks, chunk indices) is deliberately not synced: only the owner needs it, and keeping it off the wire means the remaining message text is never re-sent as a side effect of Process's own periodic auto-resync, which re-broadcasts every synced field every 5 seconds while a process runs. That means a new owner, promoted after the old one leaves, starts with empty chunk state no matter where in the transfer the old owner was. OnOwnerAbandonedProcess handles this by stopping any in-flight ready check outright (a false "all chunks equal, transfer complete" read on zeroed indices would otherwise be possible) and, if the handover happened during the inter-chunk gap, broadcasting the stopped event so receivers clean up rather than waiting forever for a chunk that will never come. OnDeserialization adds two more narrow-race recoveries for stale-packet orderings specific to this handover: a late "still in the gap" packet arriving after the takeover already resolved it, and a "zombie" state where a stale packet resurrects _isRunning=true with no actual chunk data behind it. Both are documented in detail in the source's own inline comments if you're debugging a handover edge case specifically.

Cancellation

CancelDataTransfer() is safe to call at any point, including mid-gap, and is a no-op if no transfer is active. Its behavior depends on exactly where the transfer currently is: during the gap it resets state and fires the stopped hook immediately; in the narrow window after a chunk's ready check has completed but cleanup hasn't run yet, it can't act immediately (StopReadyCheck would no-op there) so it sets an internal flag that the next OnProcessCleanup reads to abort instead of advancing to the next chunk. That same window also covers the moment right after the last chunk's ready check completes: cancelling there is a no-op too, since the transfer is already finishing on its own by that point.

Edge case worth knowing

CanAcceptTrackedPlayerAdditions is overridden here to reject adding tracked players mid-transfer (once _currentChunkIndex > 0, spanning both in-flight chunks and inter-chunk gaps). A player added after the first chunk already broadcast would fail every subsequent chunk's own player-list check, be unable to ever call SetReady, and stall the ready check permanently. Rejecting the addition outright, with a warning, is safer than letting that happen silently. Removing a tracked player mid-transfer has no equivalent restriction: that's the ordinary departure path, already handled by the mechanisms above.