@@ -37,6 +37,7 @@ const kQuickVehicleCommands = {
3737class VehicleCommands {
3838 constructor ( manager , announce , collectables , limits , playground , streamer ) {
3939 this . manager_ = manager ;
40+ this . delegates_ = new Set ( ) ;
4041
4142 this . announce_ = announce ;
4243 this . collectables_ = collectables ;
@@ -60,10 +61,9 @@ class VehicleCommands {
6061 }
6162
6263 // Command: /v [vehicle]?
63- // /v [enter/help/reset]
64- // /v [player]? [delete/health/respawn/save ]
64+ // /v [enter/help/reset/save ]
65+ // /v [player]? [delete/health/respawn]
6566 server . commandManager . buildCommand ( 'v' )
66- . restrict ( player => this . playground_ ( ) . canAccessCommand ( player , 'v' ) )
6767 . sub ( 'enter' )
6868 . restrict ( Player . LEVEL_ADMINISTRATOR )
6969 . parameters ( [ { name : 'seat' , type : CommandBuilder . NUMBER_PARAMETER ,
@@ -74,6 +74,8 @@ class VehicleCommands {
7474 . sub ( 'reset' )
7575 . restrict ( Player . LEVEL_MANAGEMENT )
7676 . build ( VehicleCommands . prototype . onVehicleResetCommand . bind ( this ) )
77+ . sub ( 'save' )
78+ . build ( VehicleCommands . prototype . onVehicleSaveCommand . bind ( this ) )
7779 . sub ( CommandBuilder . PLAYER_PARAMETER , player => player )
7880 . sub ( 'delete' )
7981 . restrict ( Player . LEVEL_ADMINISTRATOR , /* restrictTemporary= */ true )
@@ -86,15 +88,17 @@ class VehicleCommands {
8688 . sub ( 'respawn' )
8789 . restrict ( Player . LEVEL_ADMINISTRATOR )
8890 . build ( VehicleCommands . prototype . onVehicleRespawnCommand . bind ( this ) )
89- . sub ( 'save' )
90- . restrict ( Player . LEVEL_ADMINISTRATOR , /* restrictTemporary= */ true )
91- . build ( VehicleCommands . prototype . onVehicleSaveCommand . bind ( this ) )
9291 . build ( /* deliberate fall-through */ )
9392 . sub ( CommandBuilder . WORD_PARAMETER )
93+ . restrict ( player => this . playground_ ( ) . canAccessCommand ( player , 'v' ) )
9494 . build ( VehicleCommands . prototype . onVehicleCommand . bind ( this ) )
9595 . build ( VehicleCommands . prototype . onVehicleCommand . bind ( this ) ) ;
9696 }
9797
98+ // Either adds or removes the given |delegate| from the set of vehicle command delegates.
99+ addCommandDelegate ( delegate ) { this . delegates_ . add ( delegate ) ; }
100+ removeCommandDelegate ( delegate ) { this . delegates_ . delete ( delegate ) ; }
101+
98102 // ---------------------------------------------------------------------------------------------
99103
100104 // Called when the player wants to seize the vehicle that they're in. This will move them to
@@ -350,19 +354,25 @@ class VehicleCommands {
350354 // Called when the |player| executes `/v help`. Displays more information about the command, as
351355 // well as the available sub-commands to the |player|.
352356 onVehicleHelpCommand ( player ) {
353- player . sendMessage ( Message . VEHICLE_HELP_SPAWN ) ;
357+ const globalOptions = [ 'save' ] ;
358+ const vehicleOptions = [ ] ;
354359
355- if ( ! player . isAdministrator ( ) )
356- return ;
360+ if ( player . isAdministrator ( ) ) {
361+ globalOptions . push ( 'enter' , 'help' , 'reset' ) ;
362+ vehicleOptions . push ( 'health' , 'respawn' ) ;
357363
358- const globalOptions = [ 'enter' , 'help' , 'reset' ] ;
359- const vehicleOptions = [ 'health' , 'respawn' ] ;
364+ if ( ! player . isTemporaryAdministrator ( ) )
365+ vehicleOptions . push ( 'delete' ) ;
366+ }
360367
361- if ( ! player . isTemporaryAdministrator ( ) )
362- vehicleOptions . push ( 'delete' , 'save' ) ;
368+ if ( this . playground_ ( ) . canAccessCommand ( player , 'v' ) )
369+ player . sendMessage ( Message . VEHICLE_HELP_SPAWN ) ;
363370
364- player . sendMessage ( Message . VEHICLE_HELP_GLOBAL , globalOptions . sort ( ) . join ( '/' ) ) ;
365- player . sendMessage ( Message . VEHICLE_HELP_VEHICLE , vehicleOptions . sort ( ) . join ( '/' ) ) ;
371+ if ( globalOptions . length )
372+ player . sendMessage ( Message . VEHICLE_HELP_GLOBAL , globalOptions . sort ( ) . join ( '/' ) ) ;
373+
374+ if ( vehicleOptions . length )
375+ player . sendMessage ( Message . VEHICLE_HELP_VEHICLE , vehicleOptions . sort ( ) . join ( '/' ) ) ;
366376 }
367377
368378 // Called when the |player| requests the vehicle layout to be reset.
@@ -395,14 +405,62 @@ class VehicleCommands {
395405 player . sendMessage ( Message . VEHICLE_RESPAWNED , vehicle . model . name ) ;
396406 }
397407
398- // Called when the |player| executes `/v save` or `/v [player] save`, which means they wish to
399- // save the vehicle in the database to make it a persistent vehicle.
400- async onVehicleSaveCommand ( player , subject ) {
401- const vehicle = subject . vehicle ;
408+ // Called when the |player| executes `/v save`, which means they wish to save the vehicle in the
409+ // database to make it a persistent vehicle. Delegates will also be considered before making
410+ // this decision, as players might want to save e.g. their house vehicle.
411+ async onVehicleSaveCommand ( player ) {
412+ const vehicle = player . vehicle ;
402413
403- // Bail out if the |subject| is not driving a vehicle, or it's not managed by this system.
414+ // Bail out if the |player| is not currently in a vehicle - there's nothing to save. They
415+ // can see general usage guidelines after having entered one.
416+ if ( ! vehicle ) {
417+ player . sendMessage ( Message . VEHICLE_NOT_DRIVING_SELF ) ;
418+ return ;
419+ }
420+
421+ const options = [ ] ;
422+
423+ // Check whether one of the registered delegates is able to handle the vehicle's save. They
424+ // return a sequence of options, which could be displayed in a dialog.
425+ for ( const delegate of this . delegates_ )
426+ options . push ( ...await delegate . getVehicleSaveCommandOptions ( player ) ) ;
427+
428+ // If the |player| is an administrator and has the ability to save vehicles, add that to the
429+ // given |options| as well.
430+ if ( player . isAdministrator ( ) && ! player . isTemporaryAdministrator ( ) ) {
431+ options . push ( {
432+ label : `Save to the vehicle layout` ,
433+ listener : VehicleCommands . prototype . onVehiclePermanentlySaveCommand . bind ( this ) ,
434+ } ) ;
435+ }
436+
437+ // There are three options here: (1) no options, show a help message, (2) one option, fast
438+ // path and immediately call the listener, or (3) multiple options, show a dialog.
439+ if ( options . length === 1 ) return await options [ 0 ] . listener ( player , vehicle ) ;
440+
441+ if ( ! options . length ) {
442+ player . sendMessage ( Message . VEHICLE_SAVE_HELP ) ;
443+ return ;
444+ }
445+
446+ // (1) Sort the |options| based in ascending order based on the label text.
447+ options . sort ( ( lhs , rhs ) => lhs . label . localeCompare ( rhs . label ) ) ;
448+
449+ // (2) Compile a dialog with each of the |options|, and have the user pick one instead.
450+ const dialog = new Menu ( 'Vehicle options' ) ;
451+
452+ for ( const { label, listener } of options )
453+ dialog . addItem ( label , listener . bind ( null , player , vehicle ) ) ;
454+
455+ return await dialog . displayForPlayer ( player ) ;
456+ }
457+
458+ // Called when the |player|'s |vehicle| has to be permanently saved to the database. This option
459+ // is only available to permanent administrators, and will persist between sessions.
460+ async onVehiclePermanentlySaveCommand ( player , vehicle ) {
461+ // Bail out if the |player| is not driving a vehicle, or it's not managed by this system.
404462 if ( ! this . manager_ . isManagedVehicle ( vehicle ) ) {
405- player . sendMessage ( Message . VEHICLE_NOT_DRIVING , subject . name ) ;
463+ player . sendMessage ( Message . VEHICLE_NOT_DRIVING , player . name ) ;
406464 return ;
407465 }
408466
0 commit comments