11using System . Collections . Generic ;
2+ using Basis . Scripts . BasisSdk . Players ;
3+ using Basis . Scripts . Drivers ;
24using Basis . Scripts . Networking ;
35using Basis . Scripts . Networking . NetworkedAvatar ;
46using Basis . Network . Core ;
@@ -54,6 +56,7 @@ public static class BasisCameraDollyManager
5456 private static float _nextKeyframe ;
5557 private static int _lastSentCount = - 1 ;
5658 private static BasisCameraDollySync _lastSentMode = BasisCameraDollySync . LocalOnly ;
59+ private static bool _mirrorTickRequested ;
5760
5861 // ---- Lifecycle -------------------------------------------------------------------
5962
@@ -66,14 +69,24 @@ public static void Initialize()
6669 BasisNetworkPlayer . OnPlayerLeft += HandlePlayerLeft ;
6770 BasisNetworkPlayer . OnPlayerJoined -= HandlePlayerJoined ;
6871 BasisNetworkPlayer . OnPlayerJoined += HandlePlayerJoined ;
72+ BasisNetworkPlayer . OnLocalPlayerJoined -= HandleLocalPlayerJoined ;
73+ BasisNetworkPlayer . OnLocalPlayerJoined += HandleLocalPlayerJoined ;
74+ Application . quitting -= Shutdown ;
75+ Application . quitting += Shutdown ;
6976
70- ResolveNetworkId ( ) ;
77+ if ( BasisNetworkConnection . LocalPlayerIsConnected )
78+ {
79+ HandleLocalPlayerJoined ( null , null ) ;
80+ }
7181 }
7282
7383 public static void Shutdown ( )
7484 {
7585 BasisNetworkPlayer . OnPlayerLeft -= HandlePlayerLeft ;
7686 BasisNetworkPlayer . OnPlayerJoined -= HandlePlayerJoined ;
87+ BasisNetworkPlayer . OnLocalPlayerJoined -= HandleLocalPlayerJoined ;
88+ BasisNetworkPlayer . OnLocalPlayerLeft -= HandleLocalPlayerLeft ;
89+ Application . quitting -= Shutdown ;
7790
7891 if ( HasNetworkID )
7992 {
@@ -87,9 +100,19 @@ public static void Shutdown()
87100 ClearAllMirrors ( ) ;
88101 }
89102
90- private static async void ResolveNetworkId ( )
103+ /// <summary>
104+ /// Resolving the shared identifier needs a live connection, so it waits for the local player
105+ /// to be approved rather than running at load. The handler is re-armed per join because the
106+ /// network lifecycle nulls the delegate on teardown.
107+ /// </summary>
108+ private static async void HandleLocalPlayerJoined ( BasisNetworkPlayer networkPlayer , BasisLocalPlayer localPlayer )
91109 {
92110 if ( HasNetworkID ) return ;
111+ if ( ! BasisNetworkConnection . LocalPlayerIsConnected )
112+ {
113+ BasisDebug . LogError ( "Dolly manager cannot start; the local player is not connected." , LogTag ) ;
114+ return ;
115+ }
93116
94117 BasisIdResolutionResult resolution = await BasisNetworkIdResolver . ResolveAsync ( FixedNetworkIdentifier ) ;
95118 if ( ! _initialized || HasNetworkID ) return ;
@@ -104,6 +127,27 @@ private static async void ResolveNetworkId()
104127 NetworkID = resolution . Id ;
105128 HasNetworkID = true ;
106129 BasisNetworkGenericMessages . RegisterDirectHandler ( NetworkID , OnDirectNetworkMessage ) ;
130+
131+ BasisNetworkPlayer . OnLocalPlayerLeft -= HandleLocalPlayerLeft ;
132+ BasisNetworkPlayer . OnLocalPlayerLeft += HandleLocalPlayerLeft ;
133+
134+ _lastSentCount = - 1 ;
135+ _nextRoster = 0f ;
136+ _nextKeyframe = 0f ;
137+ BasisDebug . Log ( $ "Dolly manager ready (network id { NetworkID } ).", LogTag ) ;
138+ }
139+
140+ private static void HandleLocalPlayerLeft ( BasisNetworkPlayer networkPlayer , BasisLocalPlayer localPlayer )
141+ {
142+ if ( HasNetworkID )
143+ {
144+ BasisNetworkGenericMessages . UnregisterDirectHandler ( NetworkID ) ;
145+ }
146+ HasNetworkID = false ;
147+ NetworkID = 0 ;
148+ _lastSentCount = - 1 ;
149+
150+ ClearAllMirrors ( ) ;
107151 }
108152
109153 // ---- The local track --------------------------------------------------------------
@@ -126,6 +170,27 @@ public static void SetLocalTrack(BasisCameraDollyTrack track)
126170 _nextKeyframe = 0f ;
127171 }
128172
173+ /// <summary>
174+ /// Offers a track for sharing. A roster is keyed by the player who sent it, so a client
175+ /// shares one track at a time; with several cameras out the first to ask keeps the slot
176+ /// until it stops sharing, rather than the two of them withdrawing each other every frame.
177+ /// </summary>
178+ public static void ClaimLocalTrack ( BasisCameraDollyTrack track )
179+ {
180+ if ( track == null || ReferenceEquals ( _local , track ) ) return ;
181+ if ( _local != null && _local . SyncMode != BasisCameraDollySync . LocalOnly ) return ;
182+
183+ SetLocalTrack ( track ) ;
184+ }
185+
186+ /// <summary>Gives the slot up, but only for the track actually holding it.</summary>
187+ public static void ReleaseLocalTrack ( BasisCameraDollyTrack track )
188+ {
189+ if ( track == null || ! ReferenceEquals ( _local , track ) ) return ;
190+
191+ SetLocalTrack ( null ) ;
192+ }
193+
129194 /// <summary>
130195 /// Sends the local track when it is due. Called from the camera's own frame, so a client
131196 /// with no camera out costs nothing at all.
@@ -145,7 +210,7 @@ public static void Tick(float time)
145210 return ;
146211 }
147212
148- bool changed = _local . Count != _lastSentCount || _local . SyncMode != _lastSentMode ;
213+ bool changed = _local . Count != _lastSentCount || _local . SyncMode != _lastSentMode || LocalTrackIsHeld ( ) ;
149214 bool due = changed ? time >= _nextRoster : time >= _nextKeyframe ;
150215 if ( ! due ) return ;
151216
@@ -156,6 +221,20 @@ public static void Tick(float time)
156221 _lastSentMode = _local . SyncMode ;
157222 }
158223
224+ /// <summary>
225+ /// Whether a point is in somebody's hand. A drag moves points without changing the count, so
226+ /// without this a track being laid out would only reach the others at the keyframe rate.
227+ /// </summary>
228+ private static bool LocalTrackIsHeld ( )
229+ {
230+ for ( int Index = 0 ; Index < _local . Count ; Index ++ )
231+ {
232+ BasisCameraDollyWaypoint waypoint = _local . GetWaypoint ( Index ) ;
233+ if ( waypoint != null && waypoint . IsGrabbed ) return true ;
234+ }
235+ return false ;
236+ }
237+
159238 private static void BroadcastRoster ( ushort [ ] recipients )
160239 {
161240 int count = Mathf . Min ( _local . Count , BasisCameraDollyPacket . MaxPoints ) ;
@@ -172,7 +251,7 @@ private static void BroadcastRoster(ushort[] recipients)
172251 }
173252
174253 EnsureBuffer ( BasisCameraDollyPacket . RosterSize ( count ) ) ;
175- int written = BasisCameraDollyPacket . WriteRoster ( _sendBuffer , _local . SyncMode , _scratch , count ) ;
254+ int written = BasisCameraDollyPacket . WriteRoster ( _sendBuffer , _local . SyncMode , _local . Looped , _scratch , count ) ;
176255 if ( written > 0 ) Send ( written , DeliveryMethod . ReliableOrdered , recipients ) ;
177256 }
178257
@@ -182,7 +261,7 @@ private static void BroadcastWithdrawal()
182261 if ( ! HasNetworkID ) return ;
183262
184263 EnsureBuffer ( BasisCameraDollyPacket . RosterSize ( 0 ) ) ;
185- int written = BasisCameraDollyPacket . WriteRoster ( _sendBuffer , BasisCameraDollySync . LocalOnly , _scratch , 0 ) ;
264+ int written = BasisCameraDollyPacket . WriteRoster ( _sendBuffer , BasisCameraDollySync . LocalOnly , false , _scratch , 0 ) ;
186265 if ( written > 0 ) Send ( written , DeliveryMethod . ReliableOrdered , null ) ;
187266 }
188267
@@ -234,7 +313,7 @@ private static void OnDirectNetworkMessage(ushort playerId, byte[] buffer, Deliv
234313 private static void ApplyRoster ( ushort playerId , byte [ ] buffer , int length )
235314 {
236315 if ( ! BasisCameraDollyPacket . TryReadRoster ( buffer , length , _scratch ,
237- out BasisCameraDollySync mode , out int count ) )
316+ out BasisCameraDollySync mode , out bool looped , out int count ) )
238317 {
239318 return ;
240319 }
@@ -249,8 +328,9 @@ private static void ApplyRoster(ushort playerId, byte[] buffer, int length)
249328 {
250329 mirror = new BasisCameraDollyMirror ( playerId ) ;
251330 _mirrors [ playerId ] = mirror ;
331+ UpdateMirrorTickRequest ( ) ;
252332 }
253- mirror . Apply ( mode , _scratch , count ) ;
333+ mirror . Apply ( mode , looped , _scratch , count ) ;
254334 }
255335
256336 private static void ApplyPointMove ( ushort playerId , byte [ ] buffer , int length )
@@ -318,6 +398,7 @@ private static void DropMirror(ushort playerId)
318398 {
319399 mirror . Dispose ( ) ;
320400 _mirrors . Remove ( playerId ) ;
401+ UpdateMirrorTickRequest ( ) ;
321402 }
322403 }
323404
@@ -328,11 +409,41 @@ private static void ClearAllMirrors()
328409 pair . Value . Dispose ( ) ;
329410 }
330411 _mirrors . Clear ( ) ;
412+ UpdateMirrorTickRequest ( ) ;
331413 }
332414
333- /// <summary>Refreshes every mirrored track's markers. Driven from the camera's frame.</summary>
334- public static void TickMirrors ( float scale , Quaternion labelFacing )
415+ /// <summary>
416+ /// Somebody else's track has to draw on a client that has no camera of its own out — seeing
417+ /// where a shot is being laid out is the point of sharing it — so the frame clock drives the
418+ /// mirrors rather than the handheld camera. The request is held only while there is a track
419+ /// to draw, so a client with none costs nothing.
420+ /// </summary>
421+ private static void UpdateMirrorTickRequest ( )
335422 {
423+ bool wanted = _mirrors . Count > 0 ;
424+ if ( wanted == _mirrorTickRequested ) return ;
425+
426+ _mirrorTickRequested = wanted ;
427+ if ( wanted )
428+ {
429+ BasisFrameClock . OnTick += TickMirrors ;
430+ BasisFrameClock . AddRequest ( ) ;
431+ }
432+ else
433+ {
434+ BasisFrameClock . OnTick -= TickMirrors ;
435+ BasisFrameClock . RemoveRequest ( ) ;
436+ }
437+ }
438+
439+ /// <summary>Refreshes every mirrored track's markers.</summary>
440+ public static void TickMirrors ( )
441+ {
442+ float scale = BasisHeightDriver . AvatarToDefaultRatioScaledWithAvatarScale ;
443+ Quaternion labelFacing = BasisLocalCameraDriver . HasInstance && BasisLocalCameraDriver . CameraInstance != null
444+ ? BasisLocalCameraDriver . CameraInstance . transform . rotation
445+ : Quaternion . identity ;
446+
336447 foreach ( KeyValuePair < ushort , BasisCameraDollyMirror > pair in _mirrors )
337448 {
338449 pair . Value . Refresh ( scale , labelFacing ) ;
0 commit comments