关于SAMP服务端的插件开发指南[施工中...]
这个指南不是我写的,而是一位国外的开发者,我只是将它翻译并补充然后搬运到这里
原文地址:https://forum.sa-mp.com/showthread.php?t=295798
- 介绍
我决定写下这篇教程来解决插件开发中常遇到的问题,我并不是C/C++或者插件SDK的这方面的大佬,我只是简单分享我在插件开发的过程中学习到的东西,希望一旦这个主题完成.
- 常见问题解答
问题:插件是怎么制作的?
-
- 回答:使用SAMP插件SDK在C/C++中编写的,所以说如果你想学习SAMP插件开发,请一定先要对C/C++有一定的了解,并且教程的后面会提供一些关于SDK的信息来告诉你如何使用SDK
问题:我可以用C/C++以外的语言来编写插件吗?
-
- 回答:理论上是可能的,我见过有人试图将SAMP插件SDK移植到D语言上(D语言与C语言是二进制兼容的,编译后的二进制文件是可以相互连接的),虽然我从未见过用D语言编写的完整插件,但这个例子说明了,我们是可以使用支持C/C++的语言来编写插件的。
问题:如何让我的插件拥有跨平台的特性(即可以在Windows又Linux服务器上运行)?
-
- 回答:Windows和Linux是两种不同的操作系统,它们都有各自不同的API和实现。Getting your code to work on both platforms means you have to use platform independent code or a sort of framework that handles everything having to do with platforms FOR you. After you're sure your code doesn't rely on any one API, you simply have to compile your code in your desired environment (once again, this guide assumes you know how to do this).
问题:我的插件可以使用特殊手段来修改服务器内存吗?
-
- 回答:老实说,这算是个灰色地带,使用特殊手段勾住回调和调用函数时完全可以接受的。 任何修改服务器内存的操作似乎都是不受限制的,但是如果你打算制作并发布一个插件,而插件中需要使用特殊手段来钩住回调函数以及调用函数,那么请在发布之前,先向SAMP官方征得许可以并获得批准。
问题:我应该使用哪种IDE以及编译器?
-
- 回答:这完全取决于你,我个人将使用VC++已经VS2012Express作为我的Windows环境开发中的编译器和IDE,并计划使用g++作为我的Linux环境开发中的编译器,只需要随便找个不同的选择看看哪种更适合你。
问题:什么是模块定义文件?
- 回答:模块定义文件是Visual Studio中的一类特殊文件,它向链接器提供有关正在被链接的目标文件的信息,当我们在为SA-MP编写插件时,我们实际上只是用了模块定义文件中的一类语句——”EXPORTS“语句,该语句提供给链接器有关导出函数的信息,我们将在教程的后面讨论这个语句。
- 入门
当你知道我被问了多少次“我该如何入门Plugin插件开发?”时你会感到震惊。我决定将这一部分添加到整个指南中,仅仅是因为你了解一门语言并不代表你能熟练使用一个IDE,我觉得这种情况在Visual Studio中更能体现出来,对于第一次使用Visual Studio的人,他一定会被Visual Studio的复杂程度劝退,对Visual Studio对于新人来说是一个十分恐怖的地方,不过不用担心,我将手把手带着你完成这一节的内容
请注意:如果你打算选择其他的面向Windows的IDE或编译器替代Visual Studio来进行开发,那你可以放弃这种想法了哈哈,因为目前看来导出函数的唯一方法是使用.def模块定义文件(目前主流的IDE中只有微软的Visual Studio支持链接模块定义文件),我本来一开始打算在本教程中添加另外一种使用__declspec(dllexport)导出函数的方法,但这根本不起作用因为__stdcall调用约定破坏了导出函数的名称
这是本节你需要去提前准备好的东西:
任意版本的Microsoft Visual Studio: 下载
Plugin SDK (Plain): 下载
我们首先要做的第一件事是创建一个新项目。选择“文件”->“新建”->“项目”即可。一旦你创建了一个新的项目,它会问你正在创建什么类型的项目。
选择Win32 project,输入项目名称,然后按OK继续。
当你的项目设置完成后,应该会显示一个如下图所示的有关注意事项的对话框. 点击 “下一步”即可.
单击“下一步”后,您将看到弹出的对话框,询问您的应用程序类型和设置。对于类型选择“DLL(动态链接库)”,对于设置选择“空项目”。做完了这些步骤后,点击底部的“完成”按钮。
接下来我们要做的是转到我们的解决方案资源管理器。解决方案资源管理器通常位于IDE的左侧。如果不小心隐藏了它,可以通过按CTRL+ALT+L或选择“视图”->“其他窗口”->“解决方案资源管理器”再次启用它。找到解决方案资源管理器后,右键单击项目名称(在本例中为这个项目叫做“test”)并选择“属性”。
属性·页面探出头,一次点击 配置属性 -> 链接器 -> 输入,之后你需要在这个页面中添加.def模块定义文件,这个文件的文件名可以是任意的,只需要确保后缀名是.def即可,一般我们习惯用项目的名字的命名模块定义文件,例如本例中的项目名为test,所以我们就把模块定义文件取名为“test.def”即可,命名定义文件后,点击“确定”。
现在我们需要做的就是添加文件,找到资源管理器,右键单击项目的名称,然后点击“添加”,之后选择“新建项”,我首先需要添加模块定义文件,可是你会发现,添加新项中并没有模块定义文件这个选项,但是没关系,我们可以先添加普通源文件然后再把它改为我们的模块定义文件即可,我们选择“C++源文件”,然后将名称改为之前我们在连接器中的添加模块定义文件名即可,本例子中为“test.def”,请注意模块定义文件的位置应该是在解决方案下,而不是在源文件下。再添加完模块定义文件后,我们则需要添加主源文件,和之前的步骤一样,只不过我们不再需要将其改为模块定义文件,右键单击项目名称,选择“添加 ”,之后点击“添加”,选择“C++源文件”,主源文件的名字可以是任意的,只需要确保其拓展名是.cpp即可,我们一般习惯将主源文件命名为”main.cpp“。
在开始向两个空文件中写入任何内容之前,我们需要将SDK添加到项目中,我已经将下载链接放到了指南的开头。下载后,将文件解压缩到当前项目的文件夹目录中。我个人习惯创建筛选器来组织我的项目中的所有文件和代码,筛选器是Visual Studio中提供给我们的一种组织工具,它在实际上就是在项目中创建一个文件夹,与我们的Windows文件夹不同的是,只不过这个文件夹只存在于项目中,说白了就是一个虚拟的文件夹,如果你想要在项目中创建一个筛选器,请右键单击项目名称,然后点击“添加”,选择“筛选器”,然后设置筛选器的名称,本例中为SDK。
下一步是将所有SDK文件添加到项目中。要将现有文件添加到项目中,只需右键单击我们需要将其添加到的项目或文件夹,然后依次点击“添加“ -> ”现有项“,然后将SDK文件夹内的所有内容添加到项目文件夹中(注意:在单击要添加的文件时按住Ctrl键可以选择多个文件)。为了保持文件夹的一致性,我们在SDK筛选器中创建另一个筛选器,将其命名为amx,之后你需要将SDK\amx文件夹中的所有内容添加到amx筛选器中
现在是时候编译这个插件了!如果正确遵循步骤来完成,那么到目前为止则项目中应该存在一个源文件和模块定义文件,如果没有,请从头开始仔细核对一遍教程的每一个步骤。如果没有问题的话我们我们继续下面的步骤,将下面的代码复制粘贴到对应的文件中。你肯定会疑惑这些代码是干什么用的,不过不用担心,我将在下一节中解答你的疑惑。
源文件 (*.cpp):
#include "SDK\amx\amx.h"
#include "SDK\plugincommon.h"
typedef void (*logprintf_t)(char* format, ...);
logprintf_t logprintf;
extern void *pAMXFunctions;
cell AMX_NATIVE_CALL HelloWorld(AMX* amx, cell* params)
{
logprintf("来自插件中的Hello World");
return 1;
}
PLUGIN_EXPORT unsigned int PLUGIN_CALL Supports()
{
return SUPPORTS_VERSION | SUPPORTS_AMX_NATIVES;
}
PLUGIN_EXPORT bool PLUGIN_CALL Load(void **ppData)
{
pAMXFunctions = ppData[PLUGIN_DATA_AMX_EXPORTS];
logprintf = (logprintf_t) ppData[PLUGIN_DATA_LOGPRINTF];
logprintf("Test插件加载成功");
return true;
}
PLUGIN_EXPORT void PLUGIN_CALL Unload()
{
logprintf("Test插件卸载成功");
}
AMX_NATIVE_INFO PluginNatives[] =
{
{"HelloWorld", HelloWorld},
{0, 0}
};
PLUGIN_EXPORT int PLUGIN_CALL AmxLoad( AMX *amx )
{
return amx_Register(amx, PluginNatives, -1);
}
PLUGIN_EXPORT int PLUGIN_CALL AmxUnload( AMX *amx )
{
return AMX_ERR_NONE;
}
模块定义文件 (*.def):
EXPORTS
Supports
Load
Unload
AmxLoad
AmxUnload
- 代码解析
在本节中,我们将了解SDK中已经提供的一些宏定义,结构体以及函数,在开始学习之前,你应该对C/C++有一个很好的了解,因为我们将解释与SDK相关的信息,从现在开始,不会再有手把手的教学了,:-D.
-
- 模块定义文件
在我们学习源码之前,我们先来刨析一下上一节中我们创建的模块定义文件,首先模块定义文件到底是什么呢?我们知道它是Visual Studio独有的功能,但他到底在开发环节中做了什么样的角色呢?其实很简单!模块定义文件向链接器提供有关正在被链接的代码的信息,模块定义文件中有很多自己的语句及其语法规则,我想在在这里是说不完的,所以我们只讨论一个内容——EXPORTS语句。
-
- 什么是“EXPORTS”
EXPORTS是一个模块定义文件中的一类语句,它允许我们......好吧我也说不上来,正如它的字面意思一样,它允许我们,将内容导出到我们的服务端程序中,那我们为什么要这样做呢?因为我们必须要这么做,就这么简单。我们导出的函数是DLL文件中的入口点,服务端程序只能使用我们我们导出的DLL文件中的函数;如果我们不将它们导出,它们对DLL文件保持私有属性。综上所示,我们的目的是让服务端程序能够使用DLL中的函数,所以我们应该将所有服务端程序需要使用的函数使用EXPORTS语句导出。
-
- 被导出的函数
目前有下面这6个函数需要我们将其导出,而我们在我们刚才编译的项目中使用了其中5个,你可能会对类似于PLUGIN_EXPORT以及PLUGIN_CALL这些宏定义的含义感到疑惑,我能理解你,因为你可能在之前的开发中从未见到过这些函数声明中的宏定义,不要担心,我们将在本文的后面讨论这些宏定义以及另外一些同样很重要的宏定义。
Supports() | This function tells the server what capabilities our plugin will have based on what it returns. Generally we only use 3 support flags in plugins: SUPPORTS_VERSION, SUPPORTS_AMX_NATIVES, and SUPPORTS_PROCESS_TICK. |
---|---|
Load(void**) | The Load function is pretty straight forward. This is called when the plugin is loaded and gets passed an array of addresses that the plugin will use to function. The two indexes we typically use are PLUGIN_DATA_AMX_EXPORTS, and PLUGIN_DATA_LOGPRINTF. |
Unload() | Unload is called when the plugin is unloaded (server is shutdown). |
AmxLoad(AMX*) | This is called when a new AMX instance is loaded into the server. This will be called for every filterscript/gamemode! Because of this it isn't a good idea to store a single AMX instance for the entire plugin, instead use a queue/list/vector. In this function we also register our custom native functions we wish to provide PAWN with. |
AmxUnload(AMX*) | This function is called when ever an AMX instance is unloaded. If you store AMX instances, make sure you remove them. Otherwise you'll have instances to non-existing gamemodes/filterscripts. |
ProcessTick() | ProcessTick is a function that gets called on every iteration of the server's loop. People generally use this function as a method of managing time by keeping track of the amount of ticks that have passed. The SA-MP server is said to have a sleep time of 5ms, so if 50 ticks go by you have an idea of the elapsed time (5 * 50 = 250ms). Note: Anyone who uses threads in their plugins and require PAWN interaction needs to use this function to ensure PAWN isnt busy doing another task! |
-
- 宏定义和结构体
有趣的部分来了,我们先来看看代码!你可能会注意到,在上一节末尾中的小例子中,我们使用了大量的宏定义和结构体,如果你以前没有接触过Windows API编程,你可能会很不适应这种写法,觉得很混乱,有些吓人,不要担心,我们在本节中我们将对这些宏定义以及结构体进行详细的说明。
cell | "cell"是一个用typedef定义的整形类型别名.使用typedef的好处在其声明的整形始终是一个正确定值, 因为Pawn支持16位,32位和64位整形,如果使用int声明,决定其尺寸的因素将会有很多. 通常情况下,"cell"被定义为32位的整形.注意:对于无符号整形也有一个"ucell"的类型别名,但是我们很少会用到. |
---|---|
AMX_NATIVE_CALL | 这个宏定义定义了本地函数将使用的调用规则,目前它是一个空定义,因此将使用默认值. |
AMX | 这是一个由结构体定义的复合类型,我们从名字就可以知道,它存储着AMX对象的信息.此结构体包含大量与数据段以及结构体相关的信息。 此结构包含大量与数据段以及AMX相关的信息, |
PLUGIN_EXPORT | 被定义为"PLUGIN_EXTERN_C". |
PLUGIN_EXTERN_C | 如果你使用C++编译器,那么其将被定义为"extern "C"",这是为了与C语言兼容. 因为C++支持函数重载之类的东西,函数名在编译时会被篡改,这个过程叫做"Name-mangling", 这里就涉及到C++函数重载的实现原理了,我们在此不详细展开,有兴趣的可以点击这里去自行了解. 反正就是C++编译后的函数名与你起的函数名是不同的,所以我们需要去避免使用C++的函数重载特性, 最简单的方法就是在函数声明前加上"extern "C""修饰符即"PLUGIN_EXTERN_C". |
PLUGIN_CALL | 这个宏定义告诉编译器我们导出的函数使用哪种调用约定. 如果你使用面向Windows的编译器,它将会被定义为"__stdcall". 反之,它被定义为空,并使用默认约定 |
SUPPORTS_VERSION | This define is to be used in a bit mask that is returned by our "Supports()" function. This flag is used to check for compatibility with the server. |
SUPPORTS_AMX_NATIVES | This is yet another define that is to be used by our "Supports()" function. Any plugin that uses AMX functions must use this flag! Without this flag you'll get a run time 19 error due to your natives not registering with the server (amx_Register). |
SUPPORTS_PROCESS_TICK | Our last flag for our "Supports()" function. If you're going to be using the "ProcessTick()" function, you have to add this to our "Supports()" function's returned bit mask. |
PLUGIN_DATA_AMX_EXPORTS | Our last flag for our "Supports()" function. If you're going to be using the "ProcessTick()" function, you have to add this to our "Supports()" function's returned bit mask. |
PLUGIN_DATA_LOGPRINTF | 服务器加载插件时会向Load函数传递的一个指向指针数组的指针,此索引所指向的元素保存了logprintf函数的地址. 此函数输出信息到服务器控制台,并将所述信息保存到服务器日志文件中. |
AMX_NATIVE_INFO | 一个结构体,通常与amx_Regster函数一同使用,包含了函数的名称和指向函数的指针。 |
- Amx函数简介
- amx_Register
——向Amx虚拟机中注册一个本地函数
函数原型:
int amx Register(AMX *amx, AMX NATIVE INFO *list, int number);
函数参数:
- amx:指定的Amx虚拟机
- list:AMX_NATIVE_INFO类型的结构体数组
- number:数组元素的个数,如果列表以包含两个空指针的结构体结尾,则默认设置为-1
注意事项:
- 成功时此函数返回0(AMX_ERROR_NONE)。
- 如果没能找到结构体中的函数指针所指向的函数,那么函数将返回错误代码AMX_ERR_NOTFOUND。
- 你可以多次调用amx_Register函数来注册其他函数列表。
- amx_SetString
——向Amx虚拟机中存储一个字符串。
函数原型:
int amx SetString(cell *dest, char *source, int pack, int use_wchar, size_t size);
函数参数:
- dest:
- Amx函数实例
- 注册本地函数
我们提供给Pawn的任何函数都需要注册,这样Amx虚拟机才能意识到它的存在。
//一个包含了我们希望在Amx虚拟机中注册的函数的数组 AMX_NATIVE_INFO PluginNatives[] = { //在这里我们指定本机函数信息,详情参见AMX_NATIVE_INFO结构体的定义 {"HelloWorld", HelloWorld}, {0, 0} }; PLUGIN_EXPORT int PLUGIN_CALL AmxLoad( AMX *amx ) { //在这里我们向Amx虚拟机注册本地函数 //因为我们用包含了两个空指针的结构结尾了数组,所以可以指定为-1 return amx_Register(amx, PluginNatives, -1); }