To begin with, PySAMP is an evolving library that provides all the features for writing a game mode for SA:MP. But if you want to use plugins, there may be some problems with it, not all plugins are wrapped for Python. In this guide you will learn how to write a wrapper for any SA:MP plugin
Before you start, install PySAMP and the SA:MP server. You can find all the necessary links on our Discord server. For this guide, I will be creating a wrapper for the ColAndreas plugin.
- You need to install the plugin itself, so open the "Releases" tab.
- Install the
*dll
file and move it to the plugins folder.
- Edit the
server.cfg
file.
Great, you have installed the plugin. Now you need to write a wrapper.
First, create a folder (I'll call it pycolandreas
) in the server folder. In this folder create the file __init__.py
, so it should be */pycolandreas/__init__.py
.
Now open the file __init__.py
that we created earlier, in the next paragraph we will begin to break down the basics.
Open the python (*/python/__init__.py
) folder and type the following.
# */python/__init__.py
from pysamp import on_gamemode_init
@on_gamemode_init
def on_init():
print("Server started")
This code will appear every time you start the server.
> samp-server.exe
> ...
> Server started!
Before we start, we import the necessary libraries.
# */pycolandreas/__init__.py
from pysamp import call_native_function, register_callback
Most likely your code editor will report that there is no such library (PySAMP is not a pip package), but this is normal.
We will be wrapping functions using call_native_function()
. Let's wrap the very first function, which is CA_Init()
. The function should also be renamed to snake_case
.
# */pycolandreas/__init__.py
...
def col_andreas_init(): # Or ca_init()
return call_native_function("CA_Init") # Plugin function
Now back to the server, change the __init__.py
file.
# */python/__init__.py
...
from pycolandreas import col_andreas_init
@on_gamemode_init
def on_init():
print("Server started")
col_andreas_init()
You may have made a simple mistake that you will often stumble upon.
- return CallNativeFunction(name, *arguments)
- SystemError: <built-in function CallNativeFunction> returned NULL without setting an error
It is very easy to avoid it - the function that causes the error should be called in @on_gamemode_init
.
Now let's move on to wrapping the next function, we won't wrap all the functions. Next function is CA_CreateObject()
. The good thing about this function is that it accepts arguments, which will allow us to deal with them as well.
// */include/colandreas.inc
native CA_CreateObject(modelid, Float:x, Float:y, Float:z, Float:rx, Float:ry, Float:rz, bool:add = false);
As I said earlier, the function takes arguments. A minimal knowledge of Python is enough to wrap this function. I also advise you to always save the annotations. The names of the arguments must also be changed.
# */pycolandreas/__init__.py
...
def col_andreas_create_object(
model_id: int,
x: float,
y: float,
z: float,
rotation_x: float,
rotation_y: float,
rotation_z: float,
add: bool = False
):
return call_native_function(
"CA_CreateObject",
model_id,
x,
y,
z,
rotation_x,
rotation_y,
rotation_z,
add
)
You can also call this function in */python/__init__.py
, don't forget to import it from pycolandreas
.
Read the documentation of the plugin (if it exists of course) to avoid ridiculous errors or other behavior of the function. Colandreas docs says: CA_CreateObject - ONLY CREATES THE COLLISION, NOT THE IN-GAME OBJECT.
This function also returns the ID (index
) of the created object, this will come in handy later when we work with OOP, now we just wrap the CA_DeleteObject()
function and pass index
from CA_CreateObject()
.
# */pycolandreas/__init__.py
...
def col_andreas_delete_object(index: int):
return call_native_function("CA_DeleteObject", index)
Now we can delete our object, let's try.
from pycolandreas import (
col_andreas_init,
col_andreas_create_object,
col_andreas_delete_object
)
@on_gamemode_init
def on_init():
print("Server started")
col_andreas_init()
index = col_andreas_create_object()
col_andreas_delete_object(index)
As you can see it's pretty simple.
You will often encounter different types of data, so without explaining them all, here is a simple table.
Pawn | Python |
---|---|
Float |
float |
Integer |
int |
Boolean |
bool |
[] |
str or list |
Examples will be just below.
Float: x => x: float
Integer: x => x: int
Boolean: x => x: bool
type[] => x: str
types[] => x: list
Notice the difference between str
and list
. When you write your own wrapper, you can easily notice where str and list are used.
Now you will learn how to work with OOP. In fact, there is nothing complicated, we have already done a wrapper for two functions, the same functions we are going to use now. First, let's create a file, call it caobject.py
(*/pycolandreas/caobject.py
) to make it clearer to you.
Now create a class, name it whatever you like, I'll call it CAObject()
to avoid confusion with pysamp.object.Object
.
# */pycolandreas/caobject.py
from pycolandreas import (
col_andreas_create_object,
col_andreas_delete_object
)
class CAObject():
def __init__(self, index):
self.index = index
Now let's create a function that will create an object. We will also need to change its name to be CAObject.create()
, because CAObject.create_object()
makes no sense.
# */pycolandreas/caobject.py
...
@classmethod
def create(
cls,
model_id: int,
x: float,
y: float,
z: float,
rotation_x: float,
rotation_y: float,
rotation_z: float,
add: bool = False
):
return cls(
col_andreas_create_object(
model_id,
x,
y,
z,
rotation_x,
rotation_y,
rotation_z
)
)
You end up with the following code.
# */pycolandreas/caobject.py
from pycolandreas import (
col_andreas_create_object,
col_andreas_delete_object
)
class CAObject():
def __init__(self, index):
self.index = index
@classmethod
def create(
cls,
model_id: int,
x: float,
y: float,
z: float,
rotation_x: float,
rotation_y: float,
rotation_z: float,
add: bool = False
):
return cls(col_andreas_create_object)
Now our index
is 1, we can write a second function to delete the object.
# */pycolandreas/caobject.py
...
def delete(self):
return col_andreas_delete_object(self.index)
Now if we want to create an object in the main file (*/python/__init__.py
), we have to write:
# */python/__init__.py
from pysamp import on_gamemode_init
from pycolandreas.caobject import CAObject
@on_gamemode_init
def on_init():
ca_object = CAObject.create(...)
It's not hard, is it? In short, here is your procedure:
- Wrap functions in
__init__.py
usingcall_native_function()
- Make classes and write functions that will call wrapped functions from
__init__.py
.
Some functions take an argument with a &
(&Float: x
, etc.) sign. I will now tell you how to work with them. PySAMP does not currently have any support for pass-by-refernce.
Many modules have functions whose value we can "save" to avoid pass-by-refernce. To make it as clear as possible, look at the PySAMP-streamer wrapper.
class DynamicActor:
def __init__(
self, id, x=None, y=None, z=None, rotation=None, health=None
) -> None:
self.id = id
self._x = x
self._y = y
self._z = z
self._rotation = rotation
self._health = health
@classmethod
def create(
cls,
model_id: int,
x: float,
y: float,
z: float,
rotation: float,
invulnerable: bool = True,
health: float = 100.0,
world_id: int = -1,
interior_id: int = -1,
player_id: int = -1,
stream_distance: float = 200.0,
area_id: int = -1,
priority: int = 0,
) -> "DynamicActor":
return cls(
create_dynamic_actor(
model_id,
x,
y,
z,
rotation,
invulnerable,
health,
world_id,
interior_id,
player_id,
stream_distance,
area_id,
priority,
),
x,
y,
z,
rotation,
health,
)
Here, in addition to returning the id of the created actor to the class, we also return x
, y
, z
, rotation
, health
. At the moment this is one of the easiest, if not the only way to avoid pass-by-reference.
Almost all plugins have their own callbacks, you need to register them, for this there is a built-in function register_callback()
. Let's break it down. Import register_callback()
from pysamp
module
from pysamp import register_callback
def register_callback(name: str, arguments: str):
...
The name argument is the name of our callback, and arguments are the arguments it takes.
For example, there is a callback CA_OnGameModeExit()
, let's try to wrap it.
# */colandreas/__init__.py
...
def register_callback("CA_OnGameModeExit", "")
We pass an empty string since this callback requires no arguments. But this is not always the case, the table below shows the designation and type of data.
Format | Type |
---|---|
b |
bool |
s |
string |
i/d |
int |
f |
float |
For example, there is a callback (taken from another plugin) forward Streamer_OnPluginError(const error[]);
. Since you already know that const error[]
-> error: str
, it becomes clear that you will need to use s
format. Let's try.
def register_callback("Streamer_OnPluginError", "s")
Nothing complicated, I also recommend creating a separate function and registering callbacks in it.
# */colandreas/__init__.py
...
def register_callbacks():
register_callback("CA_OnGameModeExit", "")
...
The register_callbacks()
function must be called in @on_gamemode_init
.
from pysamp import on_gamemode_init
from pycolandreas import register_callbacks
...
@on_gamemode_init
def on_init():
register_callbacks()
...