Skip to content

Commit 8dc8ef0

Browse files
committed
Introduce a SAMPCAC feature in JavaScript
1 parent f1a93d2 commit 8dc8ef0

13 files changed

Lines changed: 222 additions & 88 deletions

File tree

data/server/callbacks.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ forward OnPlayerGuestLogin(playerId);
2323
[Deferred] forward OnPlayerSelectDynamicObject(playerid, objectid, modelid, Float:x, Float:y, Float:z);
2424
[Deferred] forward OnPlayerShootDynamicObject(playerid, weaponid, objectid, Float:x, Float:y, Float:z);
2525

26+
# SAMPCAC callbacks
27+
[Deferred] forward CAC_OnCheatDetect(player_id, cheat_id, opt1, opt2);
28+
[Deferred] forward CAC_OnPlayerKick(player_id, reason_id);
29+
[Deferred] forward CAC_OnMemoryRead(player_id, address, size, const content[]);
30+
[Deferred] forward CAC_OnGameResourceMismatch(player_id, model_id, component_type, checksum);
31+
[Deferred] forward CAC_OnScreenshotTaken(player_id);
32+
2633
# SA-MP callbacks
2734
forward OnGameModeInit();
2835
forward OnGameModeExit();

javascript/components/events/deferred_event_manager.js

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// be found in the LICENSE file.
44

55
import { PlayerEventObserver } from 'components/events/player_event_observer.js';
6+
import { SAMPCACEventObserver } from 'components/events/sampcac_event_observer.js';
67
import { Vector } from 'base/vector.js';
78

89
// Every how many milliseconds should pending deferred events be read from the server.
@@ -13,7 +14,9 @@ const kDeferredEventReadIntervalMs = 40; // 25Hz
1314
// JavaScript to run in completely optimized mode without a series of adapters.
1415
export class DeferredEventManager {
1516
disposed_ = false;
16-
observers_ = new Set();
17+
18+
playerObservers_ = new Set();
19+
sampcacObservers_ = new Set();
1720

1821
// ---------------------------------------------------------------------------------------------
1922

@@ -42,6 +45,51 @@ export class DeferredEventManager {
4245
let player, killer, issuer, position;
4346

4447
switch (type) {
48+
case 'CAC_OnCheatDetect':
49+
player = server.playerManager.getById(event.player_id);
50+
if (player) {
51+
for (const observer of this.sampcacObservers_) {
52+
observer.onPlayerCheatDetected(
53+
player, event.cheat_id, event.opt1, event.opt2);
54+
}
55+
}
56+
break;
57+
58+
case 'CAC_OnGameResourceMismatch':
59+
player = server.playerManager.getById(event.player_id);
60+
if (player) {
61+
for (const observer of this.sampcacObservers_) {
62+
observer.onPlayerGameResourceMismatch(
63+
player, event.model_id, event.component_type, event.checksum);
64+
}
65+
}
66+
break;
67+
68+
case 'CAC_OnMemoryRead':
69+
player = server.playerManager.getById(event.player_id);
70+
if (player) {
71+
const buffer = new Uint8Array(event.content);
72+
for (const observer of this.sampcacObservers_)
73+
observer.onPlayerMemoryRead(player, event.address, buffer);
74+
}
75+
break;
76+
77+
case 'CAC_OnPlayerKick':
78+
player = server.playerManager.getById(event.player_id);
79+
if (player) {
80+
for (const observer of this.sampcacObservers_)
81+
observer.onPlayerKicked(player, event.reason_id);
82+
}
83+
break;
84+
85+
case 'CAC_OnScreenshotTaken':
86+
player = server.playerManager.getById(event.player_id);
87+
if (player) {
88+
for (const observer of this.sampcacObservers_)
89+
observer.onPlayerScreenshotTaken();
90+
}
91+
break;
92+
4593
case 'OnDynamicObjectMoved':
4694
server.objectManager.onObjectMoved(event);
4795
break;
@@ -72,12 +120,14 @@ export class DeferredEventManager {
72120
killer = server.playerManager.getById(event.killerid);
73121

74122
if (player) {
75-
for (const observer of this.observers_)
123+
for (const observer of this.playerObservers_)
76124
observer.onPlayerDeath(player, killer, event.reason);
77125
}
78126

79127
// TODO: Migrate all the event listeners to observe |this|.
80-
dispatchEvent('playerresolveddeath', event);
128+
if (!server.isTest())
129+
dispatchEvent('playerresolveddeath', event);
130+
81131
break;
82132

83133
case 'OnPlayerSelectDynamicObject':
@@ -93,7 +143,7 @@ export class DeferredEventManager {
93143
issuer = server.playerManager.getById(event.issuerid);
94144

95145
if (player) {
96-
for (const observer of this.observers_) {
146+
for (const observer of this.playerObservers_) {
97147
observer.onPlayerTakeDamage(
98148
player, issuer, event.amount, event.weaponid, event.bodypart);
99149
}
@@ -106,7 +156,7 @@ export class DeferredEventManager {
106156
position = new Vector(event.fX, event.fY, event.fZ);
107157

108158
if (player) {
109-
for (const observer of this.observers_) {
159+
for (const observer of this.playerObservers_) {
110160
observer.onPlayerWeaponShot(
111161
player, event.weaponid, event.hittype, event.hitid, position);
112162
}
@@ -124,23 +174,26 @@ export class DeferredEventManager {
124174

125175
// Adds the given |observer| to those receiving player events.
126176
addObserver(observer) {
127-
if (!(observer instanceof PlayerEventObserver))
177+
if (observer instanceof PlayerEventObserver)
178+
this.playerObservers_.add(observer);
179+
else if (observer instanceof SAMPCACEventObserver)
180+
this.sampcacObservers_.add(observer);
181+
else
128182
throw new Error(`The given observer (${observer}) must inherit PlayerEventObserver.`);
129-
130-
this.observers_.add(observer);
131183
}
132184

133-
// Removes the given |observer| from those receiving player events.
185+
// Removes the given |observer| from those receiving any kinds of events.
134186
removeObserver(observer) {
135-
if (!(observer instanceof PlayerEventObserver))
136-
throw new Error(`The given observer (${observer}) must inherit PlayerEventObserver.`);
137-
138-
this.observers_.delete(observer);
187+
this.playerObservers_.delete(observer);
188+
this.sampcacObservers_.delete(observer);
139189
}
140190

141191
// ---------------------------------------------------------------------------------------------
142192

143193
dispose() {
144194
this.disposed_ = true;
195+
196+
this.playerObservers_.clear();
197+
this.sampcacObservers_.clear();
145198
}
146199
}

javascript/components/events/mock_deferred_event_manager.js

Lines changed: 20 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,83 +2,41 @@
22
// Use of this source code is governed by the MIT license, a copy of which can
33
// be found in the LICENSE file.
44

5-
import { PlayerEventObserver } from 'components/events/player_event_observer.js';
5+
import { DeferredEventManager } from 'components/events/deferred_event_manager.js';
66
import { ScopedCallbacks } from 'base/scoped_callbacks.js';
77

88
// Mock implementation of the DeferredEventManager that has a similar interface, but is powered
99
// through events fired through `dispatchEvent()` rather than the server's own deferred system.
10-
export class MockDeferredEventManager {
10+
export class MockDeferredEventManager extends DeferredEventManager {
1111
callbacks_ = null;
12-
observers_ = new Set();
1312

1413
constructor() {
15-
this.callbacks_ = new ScopedCallbacks();
16-
this.callbacks_.addEventListener(
17-
'playerresolveddeath', MockDeferredEventManager.prototype.onPlayerDeath.bind(this));
18-
this.callbacks_.addEventListener(
19-
'playertakedamage', MockDeferredEventManager.prototype.onPlayerTakeDamage.bind(this));
20-
this.callbacks_.addEventListener(
21-
'playerweaponshot', MockDeferredEventManager.prototype.onPlayerWeaponShot.bind(this));
22-
}
23-
24-
// ---------------------------------------------------------------------------------------------
25-
26-
onPlayerDeath(event) {
27-
const player = server.playerManager.getById(event.playerid);
28-
const killer = server.playerManager.getById(event.killerid);
14+
super();
15+
16+
const kCallbacks = [
17+
{ event: 'cac_oncheatdetect', type: 'CAC_OnCheatDetect' },
18+
{ event: 'cac_ongameresourcemismatch', type: 'CAC_OnGameResourceMismatch' },
19+
{ event: 'cac_onmemoryread', type: 'CAC_OnMemoryRead' },
20+
{ event: 'cac_onplayerkick', type: 'CAC_OnPlayerKick' },
21+
{ event: 'cac_onscreenshottaken', type: 'CAC_OnScreenshotTaken' },
22+
{ event: 'playerresolveddeath', type: 'OnPlayerResolvedDeath' },
23+
{ event: 'playertakedamage', type: 'OnPlayerTakeDamage' },
24+
{ event: 'playerweaponshot', type: 'OnPlayerWeaponShot' },
25+
];
2926

30-
if (player) {
31-
for (const observer of this.observers_)
32-
observer.onPlayerDeath(player, killer, event.reason);
33-
}
34-
}
35-
36-
onPlayerTakeDamage(event) {
37-
const player = server.playerManager.getById(event.playerid);
38-
const issuer = server.playerManager.getById(event.issuerid);
39-
40-
if (player) {
41-
for (const observer of this.observers_) {
42-
observer.onPlayerTakeDamage(
43-
player, issuer, event.amount, event.weaponid, event.bodypart);
44-
}
45-
}
46-
}
47-
48-
onPlayerWeaponShot(event) {
49-
const player = server.playerManager.getById(event.playerid);
50-
const position = new Vector(event.fX, event.fY, event.fZ);
51-
52-
if (player) {
53-
for (const observer of this.observers_) {
54-
observer.onPlayerWeaponShot(
55-
player, event.weaponid, event.hittype, event.hitid, position);
56-
}
27+
this.callbacks_ = new ScopedCallbacks();
28+
for (const { event, type } of kCallbacks) {
29+
this.callbacks_.addEventListener(
30+
event, MockDeferredEventManager.prototype.handleSingleEvent.bind(this, type));
5731
}
5832
}
5933

6034
// ---------------------------------------------------------------------------------------------
6135

62-
// Adds the given |observer| to those receiving player events.
63-
addObserver(observer) {
64-
if (!(observer instanceof PlayerEventObserver))
65-
throw new Error(`The given observer (${observer}) must inherit PlayerEventObserver.`);
66-
67-
this.observers_.add(observer);
68-
}
69-
70-
// Removes the given |observer| from those receiving player events.
71-
removeObserver(observer) {
72-
if (!(observer instanceof PlayerEventObserver))
73-
throw new Error(`The given observer (${observer}) must inherit PlayerEventObserver.`);
74-
75-
this.observers_.delete(observer);
76-
}
77-
78-
// ---------------------------------------------------------------------------------------------
79-
8036
dispose() {
8137
this.callbacks_.dispose();
8238
this.callbacks_ = null;
39+
40+
super.dispose();
8341
}
8442
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
// Observer for SAMPCAC-related events issued as deferred events. Only the Player instance will be
6+
// validated, other arguments will be passed-through as-is.
7+
export class SAMPCACEventObserver {
8+
// Called when the |player| has been detected to use the given |cheatId|. Additional information
9+
// and specifics could be found in |option1| and |option2|.
10+
onPlayerCheatDetected(player, cheatId, option1, option2) {}
11+
12+
// Called when the |modelId| for the |player| has been modified. The |checksum| identifies what
13+
// the actual value is, in case we might want to whitelist it.
14+
onPlayerGameResourceMismatch(player, modelId, componentType, checksum) {}
15+
16+
// Called when the |player| has been kicked for the given |reason|.
17+
onPlayerKicked(player, reason) {}
18+
19+
// Called when the memory at the given |address| has been read in their GTA_SA.exe memory space,
20+
// with the actual memory contents being written to |buffer| as an Uint8Buffer.
21+
onPlayerMemoryRead(player, address, buffer) {}
22+
23+
// Called when the |player| has taken a screenshot.
24+
onPlayerScreenshotTaken(player) {}
25+
}

javascript/features/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Feature | Code Health | Description
3939
[Limits](limits/) | ✔ 2020 | Centrally decides on limitations for various features, e.g. teleportation.
4040
[Nuwani](nuwani/) | ✔ 2020 | Provides our ability to interact with IRC and Discord.
4141
[PlayerStats](player_stats/) | ✔ 2020 | Provides the `Player.stats` supplement, tracks player metrics.
42+
[Sampcac](sampcac/) | ✔ 2020 | Integration with the SAMPCAC anti cheat system, optional for players.
4243
[Settings](settings/) | ✔ 2020 | Manages persistent settings on the server.
4344

4445
## 2. Low-level features
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SAMPCAC Integration
2+
Las Venturas Playground has an optional integration with the SAMPCAC library, which used to be
3+
documented on SAMPCAC.xyz which has since gone away.
4+
5+
While its own detectors are unmaintained, it does still detect a fair amount of the known cheating
6+
tools out there, and has the ability to checksum GTA: San Andreas memory regions on the player's
7+
client which can aid in writing further, manual detectors.
8+
9+
Las Venturas Playground will not mandate use of SAMPCAC, but we can request players under suspicion
10+
to use it, and introduce rewards for players who do use it, as it can be seen as a sign of trust.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
import { SAMPCACEventObserver } from 'components/events/sampcac_event_observer.js';
6+
7+
// Monitors incoming events from SAMPCAC and deals with them appropriately. Is fed events from the
8+
// DeferredEventManager, as none of them are either time-critical or cancelable.
9+
export class EventMonitor extends SAMPCACEventObserver {
10+
constructor() {
11+
super();
12+
13+
server.deferredEventManager.addObserver(this);
14+
}
15+
16+
// ---------------------------------------------------------------------------------------------
17+
// SAMPCACEventObserver implementation:
18+
// ---------------------------------------------------------------------------------------------
19+
20+
// TODO...
21+
22+
// ---------------------------------------------------------------------------------------------
23+
24+
dispose() {
25+
server.deferredEventManager.removeObserver(this);
26+
}
27+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
import { EventMonitor } from 'features/sampcac/event_monitor.js';
6+
import { Feature } from 'components/feature_manager/feature.js';
7+
8+
// Base for our integration with the SAMP-CAC anticheat tool, which is optional on Las Venturas
9+
// Playground. Keeps track of whether players have it enabled, and if so, whether they're clean.
10+
export default class SAMPCAC extends Feature {
11+
monitor_ = null;
12+
13+
constructor() {
14+
super();
15+
16+
// This is a foundational feature, as anticheat is core to the experience.
17+
this.markFoundational();
18+
19+
// The EventMonitor is responsible for monitoring incoming events from SAMPCAC, and to
20+
// handle them appropriately depending on what they describe.
21+
this.monitor_ = new EventMonitor();
22+
}
23+
24+
dispose() {
25+
this.monitor_.dispose();
26+
this.monitor_ = null;
27+
}
28+
}

javascript/main.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ testRunner.run('.*\.test\.js').then(time => {
4242
'limits',
4343
'nuwani',
4444
'player_stats',
45+
'sampcac',
4546
'settings',
4647

4748
// -----------------------------------------------------------------------------------------
@@ -52,14 +53,14 @@ testRunner.run('.*\.test\.js').then(time => {
5253
// -----------------------------------------------------------------------------------------
5354

5455
'collectables',
56+
'streamer',
5557

5658
// -----------------------------------------------------------------------------------------
5759
// Regular features
5860
// -----------------------------------------------------------------------------------------
5961

6062
// Low level features, which may only depend on each other and foundational features.
6163
'abuse', 'announce', 'economy', 'location', 'minigames',
62-
'streamer',
6364

6465
// Gang-related features
6566
'gang_chat', 'gang_zones', 'gangs',

0 commit comments

Comments
 (0)