Skip to content

Commit ae4c3da

Browse files
committed
Add an experimental /scanall command
1 parent 2c58032 commit ae4c3da

File tree

5 files changed

+100
-0
lines changed

5 files changed

+100
-0
lines changed

data/messages.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@
541541
"POSITIONING_FORWARD": "@success I like to move it, move it (forward)!",
542542

543543
"PUNISHMENT_ADMIN_UNBAN": "%s (Id:%d) has unbanned %s: %s",
544+
"PUNISHMENT_SCAN_ALL_STARTING": "{CCCCCC}* Started scanning %d players, this could take several seconds...",
544545
"PUNISHMENT_SCAN_ERROR_NPC": "@error It's not possible to scan non-player characters.",
545546
"PUNISHMENT_SCAN_RELOADED_ADMIN": "%s (Id:%d) has reloaded the SAMPCAC configuration.",
546547
"PUNISHMENT_SCAN_RELOADED": "@success The SAMPCAC configuration has been reloaded.",

javascript/features/punishments/punishment_commands.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ export class PunishmentCommands {
4444
.build(PunishmentCommands.prototype.onScanReloadCommand.bind(this))
4545
.parameters([{ name: 'player', type: CommandBuilder.kTypePlayer }])
4646
.build(PunishmentCommands.prototype.onScanCommand.bind(this));
47+
48+
// /scanall
49+
server.commandManager.buildCommand('scanall')
50+
.description('Scans all players on the server for possible cheating.')
51+
.restrict(Player.LEVEL_MANAGEMENT)
52+
.build(PunishmentCommands.prototype.onScanAllCommand.bind(this));
4753
}
4854

4955
// ---------------------------------------------------------------------------------------------
@@ -201,6 +207,74 @@ export class PunishmentCommands {
201207
await dialog.displayForPlayer(player);
202208
}
203209

210+
// Scans all players on the server for possible cheating. Will show a dialog of all in-game
211+
// human players, their SA-MP version and whether anything was detected.
212+
async onScanAllCommand(player) {
213+
const resultPromises = [];
214+
215+
// (1) Start scans for all human players connected to the server.
216+
for (const target of server.playerManager) {
217+
if (target.isNonPlayerCharacter())
218+
continue;
219+
220+
resultPromises.push(this.sampcac_().detect(target));
221+
}
222+
223+
player.sendMessage(Message.PUNISHMENT_SCAN_ALL_STARTING, resultPromises.length);
224+
225+
// (2) Wait until all the scans have completed. They may not be complete.
226+
const results = await Promise.all(resultPromises);
227+
const dialog = new Menu('Scan results', [
228+
'Player',
229+
'Version',
230+
'Minimized',
231+
'Result'
232+
], { pageSize: 50 /* just in the odd case that there's >50 people in-game... */ });
233+
234+
for (const result of results) {
235+
const nickname = format(
236+
'{%s}%s', result.player.colors.currentColor.toHexRGB(), result.player.name);
237+
238+
let version = null;
239+
240+
// (a) Format the SA-MP version used. SAMPCAC users will be highlighted in green, where
241+
// users for whom detections are supported will be yellow. All others will be red.
242+
if (result.sampcacVersion)
243+
version = '{4CAF50}' + result.version;
244+
else if (result.supported)
245+
version = '{CDDC39}' + result.version;
246+
else
247+
version = '{FF5722}' + result.version;
248+
249+
const minimized = !!result.minimized ? '{FF5722}yes' : '{9E9E9E}no';
250+
const detections = [ ...result.detectors ].filter(detector => {
251+
return detector[1] === DetectorResults.kResultDetected;
252+
});
253+
254+
let detectionResult = null;
255+
256+
// (b) Format the detection result. There either are detections (red), no detections
257+
// (green), or an unknown result because scans are not supported (grey).
258+
if (detections.length > 1)
259+
detectionResult = `{FF5722}${detectionResult.length} detections`;
260+
else if (detections.length === 1)
261+
detectionResult = `{FF5722}1 detection`;
262+
else if (!result.supported)
263+
detectionResult = `{9E9E9E}unknown`;
264+
else
265+
detectionResult = `{4CAF50}no detections`;
266+
267+
// (c) Add the data to the dialog. Selecting a particular player will act as if the
268+
// administrator executed the `/scan` command on them, for more details.
269+
dialog.addItem(nickname, version, minimized, detectionResult, () => {
270+
return this.onScanCommand(player, result.player);
271+
});
272+
}
273+
274+
// (3) Display the results dialog to the current |player|.
275+
dialog.displayForPlayer(player);
276+
}
277+
204278
// Called when someone wishes to reload the SAMPCAC definition file. Generally only needed when
205279
// changes have been made, but it's undesirable to reload the entire feature.
206280
onScanReloadCommand(player) {

javascript/features/punishments/punishment_commands.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,25 @@ describe('PunishmentCommands', (it, beforeEach) => {
132132
assert.isTrue(await gunther.issueCommand('/scan reload'));
133133
assert.equal(gunther.messages.pop(), Message.PUNISHMENT_SCAN_RELOADED);
134134
});
135+
136+
it('should be able to run memory scans on all players', async (assert) => {
137+
gunther.level = Player.LEVEL_MANAGEMENT;
138+
139+
// (2) Scans should return after a sensible amount of time, and explain that to the player.
140+
gunther.respondToDialog({ response: 0 /* dismiss */ });
141+
142+
const [ commandResult, advanceResult ] = await Promise.all([
143+
gunther.issueCommand('/scanall'),
144+
server.clock.advance(30 * 1000 /* some excessively long amount of time*/),
145+
]);
146+
147+
assert.isTrue(commandResult);
148+
assert.equal(gunther.messages.length, 1);
149+
assert.equal(
150+
gunther.messages[0],
151+
Message.format(Message.PUNISHMENT_SCAN_ALL_STARTING, server.playerManager.count));
152+
153+
assert.includes(gunther.lastDialog, '0.3.7-R4-mock');
154+
assert.includes(gunther.lastDialog, 'Lucy');
155+
});
135156
});

javascript/features/sampcac/detector_manager.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export class DetectorManager {
6767
const results = new DetectorResults();
6868

6969
// (1) Populate the meta-data fields of the results.
70+
results.player = player;
7071
results.version = player.version;
7172

7273
// Append "-R1" to distinguish from R2, R3 and R4, which do include the revision.

javascript/features/sampcac/detector_results.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export class DetectorResults {
1414
// Section: Meta-information about the player
1515
// ---------------------------------------------------------------------------------------------
1616

17+
// The player for whom this scan was started.
18+
player = null;
19+
1720
// Version of the SA-MP client that they're using.
1821
version = null;
1922

0 commit comments

Comments
 (0)