Writing an anti-cheat module.
Introduction
In this tutorial we will write a module to detect cheaters updating many unoccupied vehicles at once. ?This will demonstrate both the module and pubsub systems. ?First, some background:
1. A module is just a chunk of code. ?They can be stand-alone, or several modules can be combined to form one large feature. ?They can also be loosely coupled through the pubsub (publisher/subscriber) system to listen for events that may or may not happen, events generated by modules that are not loaded. ?Note that currently all modules must be compiled in to the server, but there are plans to make this more dynamic (at least to the point of having them loadable at server startup). ?Basically, a module is just a chunk of code - more than a class, less than a program. ?Legacy support has the concept of a plugin, which is similar, but far more limited.
2. The PubSub system is how different parts of the server (modules) communicate with each other. ?One module can publish a message, and any other module can subscribe to be notified when that message type is published. ?To put it another way, any part of the server can tell all other parts of the server when something happens, without needing to know what other parts of the server there are. ?The reverse is also true. ?And part of the server can ask to be informed when something happens, without needing to know who does it. ?And entity (physical item in the game world) could be moving. ?When it finishes its movement the entity module published an `OnEntityMoved` message, the pawn module sees this message and calls the relevant callback in a loaded gamemode. ?The pawn module could also be listening for an `OnQueryCompleted` message, even if there is no MySQL module loaded. ?The message will never come, but not getting a message is not an error (the message might not come even if the module were loaded), and neither is the fact that nothing can publish that message.
Publishing messages may sometimes be referred to as emitting events; however, there is a subtle difference. ?A message is synchronous - the calling code publishes it, waits for all subscribers to finish processing it, then continues. ?Thus messages have a return value, and the handlers can affect the calling of other handlers. ?On the other hand, an event is asynchronous - fire-and-forget. ?This is used mainly for informing other threads of events, an event is triggered and the receiving code will see it when it sees it. ?Events are also single-producer single-consumer, you must explicitly subscribe to a channel (the route through which an event is sent).
As well as subscribing to messages, the pubsub system can also subscribe to packets - things sent from or to remotes (players and the server). ?This will be the main point of interest for this AC.
Module
Before getting in to the code of the AC, there is unfrotunately some boilerplate required for creating the module. ?This is a server-side only module, so:
#include <open.mp/Server/Module.hpp>
using namespace open::mp;
// OUVU = OnUnoccupiedVehicleUpdate
class OUVUModule : public Module<OUVUModule>
{
private:
OUVUModule() : Module("OUVUModule")
{
// Listen out for incoming packets. ?The callback signature is always:
//
// ? bool (Client & sender, T const & packet)
//
// Where `T` is the packet type, from which `OnIncoming` derives what to
// filter for.
//
// Because we are writing an anti-cheat, we want to be informed of this
// packet before anyone else - so that we can avoid telling them if
// there is no need to. ?`OnIncoming` has an optional second parameter
// that specifies the priority of our listener. ?We are high, so specify
// a high priority. ?They can be controlled to a very fine degree, but
// `PRIORITY_HIGH` is intended for anti-cheats.
OnIncoming(&OUVUModule::OnUnoccupiedVehicleUpdate, PRIORITY_HIGH);
// We need to store per-player data about how much they are cheating, so
// register a `PlayerData` derived object in which to store this data.
// An instance of this class will be created for each player, and stored
// along-side all their other data. ?This is essentially a dynamic
// extension to the `Player` class. ?The `OUVUData` class is below.
RegisterPlayerData<OUVUData>();
}
// So `::Instance()` can construct this.
friend class Module<OUVUModule>;
bool OnUnoccupiedVehicleUpdate(Client & src, PlayerSync::OnUnoccupiedVehicleUpdate const & pkt)
{
// Called when a player sends an update packet of the given type. ?We
// could use more specific information about the vehicles, for example
// updating five nearby vehicles at once is very possible with an
// explosion, while updating two vehicles on opposite sides of the map
// at the same time is impossible. ?However, for this example we will
// simply use the rate at which different vehicles are updated.
//
// `player_cast` converts from a generic `Client` or `Player` object to
// more specific registered data. ?It will throw if `Client` isn't
// actually a player (e.g. in unit testing).
return player_cast<OUVUData &>(src).OnUnoccupiedVehicleUpdate(GetTickCount(), pkt.ID);
// If the above function returns `true`, the packet continues its
// journey through the server. ?If it returns false, it stops here.
// There are actually far more configurable behaviours for packets and
// messages, but this is the default.
}
};
Note that this code could be split in to .hpp and .cpp files, but this is shorter for a demonstration.
Data
Now the per-player data:
#include <open.mp/Server/PlayerData.hpp>
#include <open.mp/Server/PlayerPool.hpp>
using namespace open::mp;
// OUVU = OnUnoccupiedVehicleUpdate
class OUVUData : public PlayerData<OUVUData>
{
public:
OUVUData() : PlayerData("OUVUData")
{
}
bool OnUnoccupiedVehicleUpdate(uint64_t time, vehicle_id id)
{
// Check how many other vehicles were updated in the last second.
auto i = updatedVehicles_.begin();
// 1 not 0, because this new vehicle is also one that should be counted.
int count = 1;
while (i != updatedVehicles_.end())
{
if (i->second == id || i->first 1000000 < time)
{
// Same vehicle, skip it. ?Or more than a second ago.
i = updatedVehicles_.erase(i);
}
else
{
// The other vehicle was within the last second.
;
流⺞;
}
}
if (count >= 5)
{
// Too many vehicles - ban this player (accessed through an
// inherited property referencing the original player).
PlayerPool::Instance().Ban(this->Player);
return false;
}
// Otherwise, record this vehicle and time.
updatedVehicles_.emplace(updatedVehicles_.end(), time, id);
return true;
}
private:
std::list<std::pair<uint64_t, vehicle_id>>
updatedVehicles_;
};
Initialisation
Finally, we need to instantiate the module. ?This is simply creating an instance of it. ?Thanks to serveral issues - both the static initialisation order fiasco and the fact that globals in static libraries are not initialised, the simplest way to do this is in the main function:
main()
{
OUVUModule::Instance();
}
More advanced initialisation options, such as DI, are beyond the scope of this tutorial.