Saltar al contenido principal

DataSender

Tsvrc.DataTransfer.DataSender puts real networking under ChunkedTransferSession's sequencing hooks: it translates OnChunkSequenceStarted/Stopped/Completed into [NetworkCallable] broadcasts to every player in the instance, and exposes them as three events (OnDataTransferStartedEvent, ...StoppedEvent, ...CompletedEvent) plus matching protected virtual hooks (OnTransferStarted, OnTransferStopped, OnTransferCompleted; none of the three matching events update any Last* property, unlike the reception-side events further up the chain). You never call it directly. See DataTransferer for the type a consumer actually uses.

Owner-only broadcasts, verified per call

Each Notify* broadcast method checks that the caller is genuinely the object's owner before acting. Any player can invoke a [NetworkCallable] method directly, so without this a malicious call could reset every client's receiver state (corrupting the expected-sender tracking the next layer relies on) and stall the transfer for everyone. The check is skipped only during the owner's own inline broadcast (tracked by the inherited _isBroadcasting flag), because CallingPlayer isn't reliable there.

Why the rate limit is 100/second, not the default

All three broadcasts, and the chunk-delivery event defined one layer up, share a maxEventsPerSecond: 100 limit, the maximum VRChat allows. VRChat only guarantees relative delivery ordering between two event types from the same sender when neither exceeds its own rate limit: at the default rate, a rapid cancel-then-restart could let a chunk event overtake the "transfer started" notification on a remote client, which would see _transferActive still false when that chunk arrives, silently drop it, and stall forever with no automatic recovery. Keeping every event in this family at the same, generous rate is what keeps that ordering guarantee in force.

Deferred stop/complete emission

The broadcast handlers behind these events, NotifyTrackedPlayersDataTransferStopped/ Completed, call the OnTransferStopped/OnTransferCompleted hooks synchronously but don't emit the public events yet. They set a pending flag first and schedule the actual emission (_EmitDataTransferStopped/_EmitDataTransferCompleted) one frame later via SendCustomEventDelayedSeconds. This matters because, on the owner, ExecuteStop/ ExecuteComplete (from Process) are still on the call stack when the broadcast handler runs: internal cleanup hasn't run yet. If a subscriber reacted to the event synchronously by starting a new transfer, that new transfer's state would be corrupted by the still-pending cleanup of the old one. Deferring the event by a frame lets cleanup finish first.

The pending flags exist specifically to handle a new transfer starting inside that one-frame gap: NotifyTrackedPlayersDataTransferStarted clears both flags immediately, so if a subscriber restarts the transfer before the deferred call fires, the stale stop/complete event for the old transfer is suppressed rather than firing after the new one has already begun.