33// be found in the LICENSE file.
44
55import { SpectateGroup } from 'features/spectate/spectate_group.js' ;
6+ import { SpectateState } from 'features/spectate/spectate_state.js' ;
67
78// Responsible for keeping track of which players are spectating which player groups, and maintains
89// the global player group through which all players can be spectated.
910export class SpectateManager {
1011 #globalGroup_ = null ;
12+ #monitoring_ = false ;
13+ #settings_ = null ;
14+ #spectating_ = null ;
15+
16+ constructor ( settings ) {
17+ this . #settings_ = settings ;
1118
12- constructor ( ) {
1319 // Initialize the global spectate group, through which all players can be observed.
1420 this . #globalGroup_ = new SpectateGroup ( this , SpectateGroup . kSwitchAbandonBehaviour ) ;
1521
22+ // Map keyed by Player, valued by SpectateState instances for active spectators.
23+ this . #spectating_ = new Map ( ) ;
24+
1625 server . playerManager . addObserver ( this , /* replayHistory= */ true ) ;
1726 }
1827
@@ -21,15 +30,100 @@ export class SpectateManager {
2130 // ---------------------------------------------------------------------------------------------
2231
2332 // Called when the |player| should start spectating the given |group|, optionally starting with
24- // the given |targetPlayer|. All permission and ability checks should've been done already.
25- spectate ( player , group , targetPlayer = null ) {
26- // TODO: Implement this function
33+ // the given |target|. All permission and ability checks should've been done already. Returns
34+ // whether the spectation has started, which would fail iff |target| is spectating too.
35+ spectate ( player , group , target = null ) {
36+ if ( target && ! group . hasPlayer ( target ) )
37+ throw new Error ( `It's not possible to spectate players not in the SpectateGroup` ) ;
38+
39+ if ( ! target && ! group . size )
40+ throw new Error ( `It's not possible to spectate an empty SpectateGroup.` ) ;
41+
42+ // Bail out if the |target| is spectating too. Can't spectate someone who's spectating.
43+ if ( this . #spectating_. has ( target ) )
44+ return false ;
45+
46+ // If no |target| was given, pick the first player in the |group|. The |player| will be able
47+ // to move back and forth within the group as they please.
48+ if ( ! target )
49+ target = [ ...group ] [ 0 ] ;
50+
51+ // Put the |player| in the same Virtual World and interior as the |target|.
52+ player . virtualWorld = target . virtualWorld ;
53+ player . interiorId = target . interiorId ;
54+
55+ // If the |player| hasn't been put in spectator mode yet, do this now.
56+ if ( ! this . #spectating_. has ( player ) )
57+ player . spectating = true ;
58+
59+ this . #spectating_. set ( player , new SpectateState ( group , target ) ) ;
60+
61+ // Synchronize the environment of the |player|.
62+ this . synchronizeEnvironment ( player ) ;
63+
64+ // If the monitor isn't running yet, start it to keep player state updated.
65+ if ( ! this . #monitoring_)
66+ this . monitor ( ) ;
67+
68+ return true ;
69+ }
70+
71+ // Synchronizes the environment of the |player| with their target, to make sure that they stay
72+ // near each other when spectating. This includes interior and virtual world changes.
73+ synchronizeEnvironment ( player ) {
74+ const state = this . #spectating_. get ( player ) ;
75+ const target = state . target ;
76+
77+ // (1) The entity that the |player| is meant to be watching. If they're not, make it so.
78+ const targetEntity = target . vehicle ?? target ;
79+ if ( targetEntity !== state . targetEntity ) {
80+ if ( target . vehicle )
81+ player . spectateVehicle ( target . vehicle ) ;
82+ else
83+ player . spectatePlayer ( target ) ;
84+ }
85+
86+ // (2) Make sure that the |player| and |targetEntity| are in the same environment.
87+ if ( player . virtualWorld !== targetEntity . virtualWorld )
88+ player . virtualWorld = targetEntity . virtualWorld ;
89+
90+ if ( player . interiorId !== targetEntity . interiorId )
91+ player . interiorId = targetEntity . interiorId ;
92+
93+ // (3) Maybe have some sort of 3D text label?
2794 }
2895
2996 // Called when the |player| should stop spectating. This function will silently fail if they are
3097 // not currently spectating anyone, as there is no work to do.
3198 stopSpectate ( player ) {
32- // TODO: Implement this function
99+ const state = this . #spectating_. get ( player ) ;
100+ if ( ! state )
101+ return ; // the |player| is not currently spectating
102+
103+ // Clear out the |player|'s state, they won't be spectating anyone anymore.
104+ this . #spectating_. delete ( player ) ;
105+
106+ // Move the player out of spectation mode. This will respawn them.
107+ player . spectating = false ;
108+ }
109+
110+ // ---------------------------------------------------------------------------------------------
111+ // Section: spectation monitor
112+ // ---------------------------------------------------------------------------------------------
113+
114+ // Spins while there are players on the server who are spectating others. Will shut down when
115+ // the last player has stopped spectating, moving the system into idle mode.
116+ async monitor ( ) {
117+ await wait ( this . #settings_( ) . getValue ( 'playground/spectator_monitor_frequency_ms' ) ) ;
118+
119+ do {
120+ for ( const player of this . #spectating_. keys ( ) )
121+ this . synchronizeEnvironment ( player ) ;
122+
123+ // Wait for the configured interval before iterating in the next round.
124+ await wait ( this . #settings_( ) . getValue ( 'playground/spectator_monitor_frequency_ms' ) ) ;
125+
126+ } while ( this . #monitoring_ && this . #spectating_. size ) ;
33127 }
34128
35129 // ---------------------------------------------------------------------------------------------
@@ -51,9 +145,10 @@ export class SpectateManager {
51145 // Called when the given |player| has disconnected from the server. Removes them from the global
52146 // spectate group, and cleans up any remaining state if they're currently spectating someone.
53147 onPlayerDisconnect ( player ) {
54- this . #globalGroup_. removePlayer ( player ) ;
148+ if ( this . #spectating_. has ( player ) )
149+ this . #spectating_. delete ( player ) ;
55150
56- // TODO: Clean-up state if the |player| is currently spectating anyone.
151+ this . #globalGroup_ . removePlayer ( player ) ;
57152 }
58153
59154 // ---------------------------------------------------------------------------------------------
@@ -68,5 +163,13 @@ export class SpectateManager {
68163
69164 dispose ( ) {
70165 server . playerManager . removeObserver ( this ) ;
166+
167+ // Forcefully stop all current spectators from spectating. This would cause a bug for people
168+ // currently in between rounds in a game, but work fine in all other cases.
169+ for ( const player of this . #spectating_. keys ( ) )
170+ this . stopSpectate ( player ) ;
171+
172+ this . #monitoring_ = false ;
173+ this . #settings_ = null ;
71174 }
72175}
0 commit comments