Skip to content

Commit bf758a6

Browse files
committed
Multiplex the behaviour of the /v save command, remove level restrictions
1 parent 430c670 commit bf758a6

File tree

6 files changed

+109
-37
lines changed

6 files changed

+109
-37
lines changed

data/messages.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,12 +546,13 @@
546546
"VEHICLE_LOCK_UNREGISTERED": "@error Sorry, only registered players can lock vehicles. Register now on www.sa-mp.nl!",
547547
"VEHICLE_LOCKED": "@success The %s has been locked!",
548548
"VEHICLE_NOT_DRIVING": "@error %s is not currently in a vehicle managed by JavaScript.",
549-
"VEHICLE_NOT_DRIVING_SELF": "@error You need to be in a public vehicle in order to use this command.",
549+
"VEHICLE_NOT_DRIVING_SELF": "@error You need to be in a vehicle in order to use this command.",
550550
"VEHICLE_QUICK_ALREADY_DRIVING": "@error Sorry, but you're already driving a vehicle!",
551551
"VEHICLE_QUICK_COLLECTABLES": "@error Keep tagging Spray Tags and shooting Red Barrels to enable this command!",
552552
"VEHICLE_RESET": "@success The vehicle layout has been reset.",
553553
"VEHICLE_RESPAWNED": "@success The %s has been respawned.",
554554
"VEHICLE_RESPAWN_NOT_IN_VEHICLE": "@error %s is not in a vehicle, which can thus not be respawned.",
555+
"VEHICLE_SAVE_HELP": "@error You can only save vehicles when you're an administrator, have a house, or have VIP rights.",
555556
"VEHICLE_SAVE_TOO_BUSY": "@error Sorry, this area is too busy! There already are %d vehicles (max: %d) and %d models (max: %d).",
556557
"VEHICLE_SAVED": "@success The %s has been %s in the database.",
557558
"VEHICLE_SEIZE_DRIVER": "@error %s is already driving your vehicle!",

javascript/features/vehicles/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Furthermore, administrators are able to use the following commands:
2828
* **/v density**: Displays the density of vehicles within streaming radius around you.
2929
* **/v enter [seat?]**: Enters the vehicle closest to you, optionally in `seat` (0-8).
3030
* **/v reset**: Resets the server to its original vehicle layout.
31+
* **/v save**: Saves the vehicle that you're currently driving in the database.
3132
* **/v [player]? access [players/vips/administrators/management]?**: Restricts the vehicle to a
3233
particular `level`.
3334
* **/v [player]? color [0-255]? [0-255]?**: Displays or updates the colors of either your own vehicle, or
@@ -36,7 +37,6 @@ Furthermore, administrators are able to use the following commands:
3637
* **/v [player]? health [0-1000]?**: Displays or updates the health of either your own vehicle, or
3738
that of `player`.
3839
* **/v [player]? respawn**: Respawns either your own vehicle, or that of `player`.
39-
* **/v [player]? save**: Saves either your own vehicle, or that of `player`, in the database.
4040

4141
_Note that temporary administrators are not allowed to use either `/v save` or `/v delete`._
4242

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2020 Las Venturas Playground. All rights reserved.
2+
// Use of this source code is governed by the MIT license, a copy of which can
3+
// be found in the LICENSE file.
4+
5+
// Delegate for vehicle commands. Not all vehicles manageable are created by the Vehicles system,
6+
// some are created by other systems such as Houses or gang zones. They can still benefit from the
7+
// ability to save and change vehicles.
8+
export class VehicleCommandDelegate {
9+
// Called when the |player| wishes to save the vehicle they're driving. Must return a sequence
10+
// of options ({ label, listener }) that can be considered by the system.
11+
async getVehicleSaveCommandOptions(player) { return []; }
12+
}

javascript/features/vehicles/vehicle_commands.js

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const kQuickVehicleCommands = {
3737
class 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

javascript/features/vehicles/vehicle_commands.test.js

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -198,18 +198,6 @@ describe('VehicleCommands', (it, beforeEach) => {
198198
}
199199
});
200200

201-
// TODO: We'll actually want to make this available to all the players.
202-
// See the following issue: https://github.com/LVPlayground/playground/issues/330
203-
it('should limit /v to administrators only', async(assert) => {
204-
const russell = server.playerManager.getById(1 /* Russell */);
205-
assert.equal(russell.level, Player.LEVEL_PLAYER);
206-
207-
assert.isTrue(await russell.issueCommand('/v'));
208-
assert.equal(russell.messages.length, 1);
209-
assert.equal(russell.messages[0],
210-
Message.format(Message.COMMAND_ERROR_INSUFFICIENT_RIGHTS, 'specific people'));
211-
});
212-
213201
it('should support spawning vehicles by their model Id', async(assert) => {
214202
for (const invalidModel of ['-15', '42', '399', '612', '1337']) {
215203
assert.isTrue(await gunther.issueCommand('/v ' + invalidModel));
@@ -469,7 +457,7 @@ describe('VehicleCommands', (it, beforeEach) => {
469457

470458
assert.isTrue(await gunther.issueCommand('/v save'));
471459
assert.equal(gunther.messages.length, 1);
472-
assert.equal(gunther.messages[0], Message.VEHICLE_QUICK_ALREADY_DRIVING);
460+
assert.equal(gunther.messages[0], Message.VEHICLE_SAVE_HELP);
473461

474462
assert.isTrue(await gunther.issueCommand('/v delete'));
475463
assert.equal(gunther.messages.length, 2);
@@ -480,8 +468,9 @@ describe('VehicleCommands', (it, beforeEach) => {
480468
gunther.level = Player.LEVEL_PLAYER;
481469
{
482470
assert.isTrue(await gunther.issueCommand('/v help'));
483-
assert.equal(gunther.messages.length, 1);
471+
assert.equal(gunther.messages.length, 2);
484472
assert.equal(gunther.messages[0], Message.VEHICLE_HELP_SPAWN);
473+
assert.equal(gunther.messages[1], Message.format(Message.VEHICLE_HELP_GLOBAL, 'save'));
485474

486475
gunther.clearMessages();
487476
}

javascript/features/vehicles/vehicles.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ class Vehicles extends Feature {
4949
this.decorations_ = new VehicleDecorations(settings, announce);
5050
}
5151

52+
// ---------------------------------------------------------------------------------------------
53+
// Public API of the Vehicles feature
54+
// ---------------------------------------------------------------------------------------------
55+
56+
// Adds the given |delegate| to the set of delegates that can handle vehicle commands.
57+
addCommandDelegate(delegate) { this.commands_.addCommandDelegate(delegate); }
58+
59+
// Removes the given |delegate| from the set of delegates that can handle vehicle commands.
60+
removeCommandDelegate(delegate) { this.commands_.removeCommandDelegate(delegate); }
61+
62+
// ---------------------------------------------------------------------------------------------
63+
5264
dispose() {
5365
this.commands_.dispose();
5466
this.commands_ = null;

0 commit comments

Comments
 (0)