Permalink
Browse files

Restrict spawning vehicles to once per three minutes

Fixes #392
  • Loading branch information...
RussellLVP committed Oct 19, 2016
1 parent da48730 commit 0410f1321742aa6be35802a2e0740fa9691c190a
View
@@ -278,7 +278,7 @@
"VEHICLE_SAVED": "@success The %s has been %s in the database.",
"VEHICLE_SPAWN_CREATED": "@success Your %s has been created!",
"VEHICLE_SPAWN_NOT_FOUND": "@error We're not sure what vehicle {FF8282}%s{FFFFFF} is...",
"VEHICLE_SPAWN_REJECTED": "@error Sorry, you're not allowed to spawn vehicles right now!",
"VEHICLE_SPAWN_REJECTED": "@error Sorry, you can't spawn this vehicle right now because you %s.",
"VEHICLE_UNLOCK_PASSENGER": "@error You can only unlock a vehicle when you're the driver!",
"VEHICLE_UNLOCK_REDUNDANT": "@error No need to unlock your vehicle because it's not locked!",
"VEHICLE_UNLOCK_UNREGISTERED": "@error Sorry, only registered players can unlock vehicles. Register now on www.sa-mp.nl!",
@@ -35,50 +35,74 @@ class Abuse extends Feature {
const time = server.clock.monotonicallyIncreasingTime();
// (1) Administrators might be able to override teleportation limitations.
if (player.isAdministrator() && this.getSetting('tp_blocker_admin_override'))
if (player.isAdministrator() && this.getSetting('teleportation_admin_override'))
return { allowed: true };
const blockerUsageThrottle =
enforceTimeLimit ? this.getSetting('tp_blocker_usage_throttle_time') * 1000 // ms
enforceTimeLimit ? this.getSetting('teleportation_throttle_time') * 1000 // ms
: 0 /* no throttle will be applied */;
// (2) Might be subject to the per-player teleportation usage throttle.
if (!this.mitigator_.satisfiesTimeThrottle(player, time, blockerUsageThrottle, 'tp'))
return { allowed: false, reason: AbuseConstants.REASON_TIME_LIMIT };
return { allowed: false, reason: AbuseConstants.REASON_TIME_LIMIT(blockerUsageThrottle) };
const blockerWeaponFired = this.getSetting('tp_blocker_weapon_fire_time') * 1000; // ms
const blockerDamageIssued = this.getSetting('tp_blocker_damage_issued_time') * 1000; // ms
const blockerDamageTaken = this.getSetting('tp_blocker_damage_taken_time') * 1000; // ms
return this.internalProcessFightingConstraints(player, time);
}
// Reports that the |player| has been teleported through an activity that's time throttled.
reportTimeThrottledTeleport(player) {
this.mitigator_.reportTimeThrottleUsage(player, 'tp');
}
// ---------------------------------------------------------------------------------------------
// Returns whether the |player| is allowed to spawn a vehicle right now. Constraints similar to
// teleportation apply, but the actual variables can be configured separately.
canSpawnVehicle(player) {
const time = server.clock.monotonicallyIncreasingTime();
// (1) Administrators might be able to override the vehicle spawning limitations.
if (player.isAdministrator() && this.getSetting('spawn_vehicle_admin_override'))
return { allowed: true };
const blockerUsageThrottle = this.getSetting('spawn_vehicle_throttle_time') * 1000 // ms
// (2) Might be subject to the per-player vehicle spawning throttle.
if (!this.mitigator_.satisfiesTimeThrottle(player, time, blockerUsageThrottle, 'vehicle'))
return { allowed: false, reason: AbuseConstants.REASON_TIME_LIMIT(blockerUsageThrottle) };
// (3) Should having fired your weapon temporarily block teleportation?
return this.internalProcessFightingConstraints(player, time);
}
// Reports that the |player| has spawned a vehicle through one of the commands.
reportSpawnedVehicle(player) {
this.mitigator_.reportTimeThrottleUsage(player, 'vehicle');
}
// ---------------------------------------------------------------------------------------------
// Processes the common fighting-related constraints for |player|. Not to be used externally.
internalProcessFightingConstraints(player, time) {
const blockerWeaponFired = this.getSetting('blocker_weapon_fire_time') * 1000; // ms
const blockerDamageIssued = this.getSetting('blocker_damage_issued_time') * 1000; // ms
const blockerDamageTaken = this.getSetting('blocker_damage_taken_time') * 1000; // ms
// (3) Should having fired your weapon temporarily block the action?
if (!this.mitigator_.satisfiesWeaponFireConstraint(player, time, blockerWeaponFired))
return { allowed: false, reason: AbuseConstants.REASON_FIRED_WEAPON };
// (4) Should having issued damage to another player temporarily block teleportation?
// (4) Should having issued damage to another player temporarily block the action?
if (!this.mitigator_.satisfiesDamageIssuedConstraint(player, time, blockerDamageIssued))
return { allowed: false, reason: AbuseConstants.REASON_DAMAGE_ISSUED };
// (5) Should having taken damage from another player temporarily block teleportation?
// (5) Should having taken damage from another player temporarily block the action?
if (!this.mitigator_.satisfiesDamageTakenConstraint(player, time, blockerDamageTaken))
return { allowed: false, reason: AbuseConstants.REASON_DAMAGE_TAKEN };
// (6) Otherwise the |player| is allowed to teleport to wherever they wanted to go.
// (6) Otherwise the |player| is allowed to do whatever they wanted to do.
return { allowed: true };
}
// Reports that the |player| has been teleported through an activity that's time limited.
reportTimeLimitedTeleport(player) {
this.mitigator_.reportTimeThrottleUsage(player, 'tp');
}
// Returns whether the |player| is allowed to spawn a vehicle right now. The implementation of
// this method defers to whether the |player| is allowed to teleport.
canSpawnVehicle(player) {
// TODO: Spawning vehicles should be time limited as well, but it should maintain a
// different counter from the teleportation time limit.
return this.canTeleport(player, { enforceTimeLimit: false });
}
// ---------------------------------------------------------------------------------------------
dispose() {
@@ -2,6 +2,7 @@
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.
const AbuseConstants = require('features/abuse/abuse_constants.js');
const MockAbuse = require('features/abuse/test/mock_abuse.js');
const Settings = require('features/settings/settings.js');
@@ -60,7 +61,7 @@ describe('Abuse', (it, beforeEach) => {
assert.isTrue(abuse.canTeleport(gunther, { enforceTimeLimit: false }).allowed);
assert.isTrue(abuse.canTeleport(gunther, { enforceTimeLimit: true }).allowed);
abuse.reportTimeLimitedTeleport(gunther, { timeLimited: true });
abuse.reportTimeThrottledTeleport(gunther, { timeLimited: true });
assert.isTrue(abuse.canTeleport(gunther, { enforceTimeLimit: false }).allowed);
assert.isFalse(abuse.canTeleport(gunther, { enforceTimeLimit: true }).allowed);
@@ -85,4 +86,22 @@ describe('Abuse', (it, beforeEach) => {
assert.isTrue(abuse.canTeleport(gunther, { enforceTimeLimit: false }).allowed);
});
it('should be able to format time limits', assert => {
const mappings = {
1: 'second',
2: '2 seconds',
60: 'minute',
61: '1:01 minutes',
120: '2 minutes',
121: '2:01 minutes',
3600: 'hour',
7500: '2 hours'
};
for (const [seconds, description] of Object.entries(mappings)) {
assert.equal(AbuseConstants.REASON_TIME_LIMIT(seconds * 1000),
'can only do so once per ' + description);
}
});
});
@@ -4,10 +4,52 @@
class AbuseConstants {}
// Converts the |number| to a string and makes sure that it has at least two digits.
function pad(number) {
return ('0' + number).substr(-2);
}
// Formats the |time|, denoted in milliseconds, as a time span period.
function formatTimePeriod(time) {
time = Math.floor(time / 1000); // convert to seconds
if (time == 1)
return 'second';
if (time < 60)
return time + ' seconds';
const minutes = Math.floor(time / 60);
const seconds = time % 60;
if (minutes == 1 && seconds == 0)
return 'minute';
if (minutes < 60) {
if (seconds > 0)
return minutes + ':' + pad(seconds) + ' minutes';
return minutes + ' minutes';
}
const hours = Math.floor(minutes / 60);
if (hours == 1)
return 'hour';
return hours + ' hours';
}
// Returns whether the |reason| is a time-limited abuse reason.
AbuseConstants.isTimeLimit = (reason) =>
reason.includes('can only do so once per ');
// Textual descriptions about why an action has been denied.
AbuseConstants.REASON_FIRED_WEAPON = 'recently fired a weapon';
AbuseConstants.REASON_DAMAGE_ISSUED = 'recently hurt another player';
AbuseConstants.REASON_DAMAGE_TAKEN = 'recently got hurt by another player';
AbuseConstants.REASON_TIME_LIMIT = 'can only teleport once three minutes';
AbuseConstants.REASON_TIME_LIMIT = (limit) => {
return 'can only do so once per ' + formatTimePeriod(limit);
};
exports = AbuseConstants;
@@ -38,11 +38,11 @@ class AbuseNatives {
case AbuseConstants.REASON_DAMAGE_ISSUED:
case AbuseConstants.REASON_DAMAGE_TAKEN:
return TELEPORT_STATUS_REJECTED_FIGHTING;
case AbuseConstants.REASON_TIME_LIMIT:
return TELEPORT_STATUS_REJECTED_TIME_LIMIT;
}
if (AbuseConstants.isTimeLimit(teleportStatus.reason))
return TELEPORT_STATUS_REJECTED_TIME_LIMIT;
return TELEPORT_STATUS_REJECTED_OTHER;
}
@@ -53,7 +53,7 @@ class AbuseNatives {
return; // the |playerId| does not represent a connected player
if (timeLimited)
this.abuse_.reportTimeLimitedTeleport(player);
this.abuse_.reportTimeThrottledTeleport(player);
}
// ---------------------------------------------------------------------------------------------
@@ -36,13 +36,13 @@ class MockAbuse extends Abuse {
// ---------------------------------------------------------------------------------------------
// Toggles whether |player| can teleport. Only available for tests.
toggleTeleportForTests(player, enabled) {
this.disableTeleport_.set(player, enabled);
toggleTeleportForTests(player, reason) {
this.disableTeleport_.set(player, reason);
}
// Toggles whether |player| can spawn a vehicle. Only avaiable for tests.
toggleSpawnVehicleForTests(player, enabled) {
this.disableSpawnVehicle_.set(player, enabled);
toggleSpawnVehicleForTests(player, reason) {
this.disableSpawnVehicle_.set(player, reason);
}
// ---------------------------------------------------------------------------------------------
@@ -331,7 +331,7 @@ class HouseCommands {
menu.addItem(location.settings.name, player => {
this.manager_.forceEnterHouse(player, location);
this.abuse_().reportTimeLimitedTeleport(player);
this.abuse_().reportTimeThrottledTeleport(player);
// Announce creation of the location to other administrators.
this.announce_().announceToAdministrators(
@@ -6,9 +6,13 @@ const Setting = require('features/settings/setting.js');
exports = [
/** Abuse-related settings */
new Setting('abuse', 'tp_blocker_admin_override', Setting.TYPE_BOOLEAN, true, 'Should administrators override teleportation restrictions?'),
new Setting('abuse', 'tp_blocker_damage_issued_time', Setting.TYPE_NUMBER, 10, 'Number of seconds to block teleportation after issuing damage.'),
new Setting('abuse', 'tp_blocker_damage_taken_time', Setting.TYPE_NUMBER, 10, 'Number of seconds to block teleportation after having taken damage.'),
new Setting('abuse', 'tp_blocker_usage_throttle_time', Setting.TYPE_NUMBER, 180, 'Number of seconds that should be between teleportations.'),
new Setting('abuse', 'tp_blocker_weapon_fire_time', Setting.TYPE_NUMBER, 10, 'Number of seconds to block teleportation after firing your weapon.'),
new Setting('abuse', 'blocker_damage_issued_time', Setting.TYPE_NUMBER, 10, 'Number of seconds to block actions after issuing damage.'),
new Setting('abuse', 'blocker_damage_taken_time', Setting.TYPE_NUMBER, 10, 'Number of seconds to block actions after having taken damage.'),
new Setting('abuse', 'blocker_weapon_fire_time', Setting.TYPE_NUMBER, 10, 'Number of seconds to block actions after firing your weapon.'),
new Setting('abuse', 'spawn_vehicle_admin_override', Setting.TYPE_BOOLEAN, true, 'Should administrators override vehicle spawning restrictions?'),
new Setting('abuse', 'spawn_vehicle_throttle_time', Setting.TYPE_NUMBER, 180, 'Minimum number of seconds between spawning two vehicles.'),
new Setting('abuse', 'teleportation_admin_override', Setting.TYPE_BOOLEAN, true, 'Should administrators override teleportation restrictions?'),
new Setting('abuse', 'teleportation_throttle_time', Setting.TYPE_NUMBER, 180, 'Minimum number of seconds between teleporting twice.'),
];
@@ -232,9 +232,10 @@ class VehicleCommands {
return;
}
if (!this.abuse_().canSpawnVehicle(player).allowed) {
player.sendMessage(Message.VEHICLE_SPAWN_REJECTED);
return;
const spawnStatus = this.abuse_().canSpawnVehicle(player);
if (!spawnStatus.allowed) {
player.sendMessage(Message.VEHICLE_SPAWN_REJECTED, spawnStatus.reason);
return false;
}
let vehicleModel = null;
@@ -263,6 +264,9 @@ class VehicleCommands {
// Inform the player of their new vehicle having been created.
player.sendMessage(Message.VEHICLE_SPAWN_CREATED, vehicleModel.name);
// Report that the |player| has spawned a vehicle. This rate-limits their usage too.
this.abuse_().reportSpawnedVehicle(player);
// If the |vehicle| is live, teleport the |player| to the driver seat after a minor delay.
if (vehicle && vehicle.isConnected())
player.enterVehicle(vehicle, Vehicle.SEAT_DRIVER);
@@ -2,6 +2,7 @@
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.
const AbuseConstants = require('features/abuse/abuse_constants.js');
const DatabaseVehicle = require('features/vehicles/database_vehicle.js');
const MockAbuse = require('features/abuse/test/mock_abuse.js');
const MockAnnounce = require('features/announce/test/mock_announce.js');
@@ -239,7 +240,21 @@ describe('VehicleCommands', (it, beforeEach) => {
gunther.leaveVehicle();
}
// (3) Players who have collected all spray tags can use the commands.
// (3) Players are only allowed to spawn such vehicles once per three minutes.
{
assert.isTrue(await gunther.issueCommand('/nrg'));
assert.equal(gunther.messages.length, 1);
assert.equal(
gunther.messages[0], Message.format(Message.VEHICLE_SPAWN_REJECTED,
'can only do so once per 3 minutes'));
assert.isNull(gunther.vehicle);
gunther.clearMessages();
}
await server.clock.advance(180 * 1000);
// (4) Players who have collected all spray tags can use the commands.
{
toggleCommand(false);
toggleSprayTags(true);
@@ -254,7 +269,7 @@ describe('VehicleCommands', (it, beforeEach) => {
gunther.leaveVehicle();
}
// (4) Players may not be in a vehicle when using this command.
// (5) Players may not be in a vehicle when using this command.
{
assert.isTrue(createVehicleForPlayer(gunther));
assert.isNotNull(gunther.vehicle);
@@ -270,7 +285,7 @@ describe('VehicleCommands', (it, beforeEach) => {
gunther.leaveVehicle();
}
// (5) Players must be outside in the main world in order to use the command.
// (6) Players must be outside in the main world in order to use the command.
{
gunther.interiorId = 7;
@@ -283,17 +298,19 @@ describe('VehicleCommands', (it, beforeEach) => {
gunther.interiorId = 0;
}
// (6) Players must not have been refused from spawning vehicles.
await server.clock.advance(180 * 1000);
// (7) Players must not have been refused from spawning vehicles.
{
abuse.toggleSpawnVehicleForTests(gunther, false);
gunther.shoot();
assert.isTrue(await gunther.issueCommand('/inf'));
assert.equal(gunther.messages.length, 1);
assert.equal(gunther.messages[0], Message.VEHICLE_SPAWN_REJECTED);
assert.equal(gunther.messages[0], Message.format(Message.VEHICLE_SPAWN_REJECTED,
AbuseConstants.REASON_FIRED_WEAPON));
assert.isNull(gunther.vehicle);
gunther.clearMessages();
abuse.toggleSpawnVehicleForTests(gunther, true);
}
});
@@ -319,6 +336,9 @@ describe('VehicleCommands', (it, beforeEach) => {
gunther.position = gunther.vehicle.position;
gunther.clearMessages();
// Forward the clock so that the player is allowed to use spawn vehicles again.
await server.clock.advance(180 * 1000);
}
});

0 comments on commit 0410f13

Please sign in to comment.