Difference between revisions of "Ru:Introduction to SourceMod Plugins"

From AlliedModders Wiki
Jump to: navigation, search
(События)
(Updated first section, move new sections from english version to russian)
Line 3: Line 3:
 
Для получения информации о компиляции плагинов см. [[Compiling SourceMod Plugins]] (Компиляция SourceMod плагинов). Автор настоящей статьи использует редактор [http://www.crimsoneditor.com/ Crimson Editor] для написания плагинов. Вы можете использовать [http://www.pspad.com/ PSPad], [http://www.ultraedit.com/ UltraEdit], [http://notepad-plus.sourceforge.net/uk/site.htm Notepad++], [http://www.textpad.com/ TextPad], [http://sourceforge.net/projects/pawnstudio/ SourceMod IDE] или любой другой текстовый редактор.
 
Для получения информации о компиляции плагинов см. [[Compiling SourceMod Plugins]] (Компиляция SourceMod плагинов). Автор настоящей статьи использует редактор [http://www.crimsoneditor.com/ Crimson Editor] для написания плагинов. Вы можете использовать [http://www.pspad.com/ PSPad], [http://www.ultraedit.com/ UltraEdit], [http://notepad-plus.sourceforge.net/uk/site.htm Notepad++], [http://www.textpad.com/ TextPad], [http://sourceforge.net/projects/pawnstudio/ SourceMod IDE] или любой другой текстовый редактор.
  
=Структура плагина=
+
= Начиная с нуля =
Почти все плагины имеют три одинаковых элемента:
+
Откройте Ваш любимый текстовый редактор и создайте новый пустой файл. Теперь Вы можете начать писать код, используя основные функции языка, однако, Вы не можете использовать любые функции и особенности самого SourceMod, потому что компилятор ничего не знает о них. Это сделано специально, чтобы было возможно использовать SourcePawn отдельно от SourceMod. Но так как мы пишем плагин именно для SourceMod, самая лучшая идея - это в первую очередь получить доступ к оособенностям SourceMod. Этой цели можно достичь с помощью директивы <tt>#include</tt>. Она сообщает компилятору, что содержимое указанного файла нужно вставить в Ваш при компиляции.
*'''Includes''' - Позволяет получить доступ к SourceMod API, и если Вы хотите, к API от внешних SourceMod плагинов и расширений.
+
<pawn>#include <sourcemod></pawn>
*'''Info''' - Общественная информация о Вашем плагине.
+
Как это работает? Сначала обратите внимание, что имя файла мы обрамили в угловые скобки. Угловые скобки сообщают компилятору, что указанный файл нужно поискать в стандартных директориях подключаемых файлов. По-умолчанию, такая директория одна, и это '''scripting/include'''. Вы можете открыть эту папку и увидеть множество файлов с расширением <tt>.inc</tt> здесь. Всё это - подключаемые файлы SourceMod, которые включают в себя описание множества функций, тегов и других особенностей, доступных плагинам SourceMod. Подключаемые файлы, по сути, это такой же обычный текст, как и сам код плагинов, потому Вы можете без проблем открыть его любым текстовым редактором и прочитать. Вы можете обратить внимание, однако, что здесь не так много кода, чтобы эти функции SourceMod вообще работали, так как же оно работает? Они реализованы внутри ядра самого SourceMod и написаны на C++, и скомпилированы в двоичные исполняемые файлы, которые размещаются в папке '''bin'''. Так как SourcePawn-код и SourceMod-код связываются между собой, если компилятор ничего не знает о существовании последнего? Подключаемые файлы SourceMod реализованы таким образом, чтобы компилятор понимал, что реализация функций будет ''где-то в другом месте''. Компилятор понимает это, и генерирует специальный код, который вызовет внешнюю функцию. Когда SourceMod загружает Ваш плагин, он анализирует этот код и заменяет на вызов своих внутренних функций. Это называется [https://en.wikipedia.org/wiki/Dynamic_linker динамическим связыванием].
*'''Startup''' - Функция, которая осуществляет запуск процедур в Вашем плагине.
 
  
Плагин скелетной структуры выглядит следующим образом:
+
=Setting up plugin info=
<pawn>
+
Now that we got access to SourceMod features, it is time to set up the information that will be displayed via <tt>sm plugins list</tt> command. No one likes unnamed plugins. To do that we are going to look inside '''sourcemod.inc''' file and see the format that information should be declared. It's always helpful to look inside SM include files to find out information you don't know. There is also an [http://docs.sourcemod.net/api/ API documentation] but it can be outdated and it only has SM core files so if your plugin is going to use any third party extension or another plugin, you will have to study inc files. So, open '''sourcemod.inc''' and scroll down a bit until you see this:
#include <sourcemod>
+
<sourcepawn>/**
 +
* Plugin public information.
 +
*/
 +
struct Plugin
 +
{
 +
  public const char[] name; /**< Plugin Name */
 +
  public const char[] description; /**< Plugin Description */
 +
  public const char[] author; /**< Plugin Author */
 +
  public const char[] version; /**< Plugin Version */
 +
  public const char[] url; /**< Plugin URL */
 +
};</sourcepawn>
 +
and this:
 +
<sourcepawn>/**
 +
* Declare this as a struct in your plugin to expose its information.
 +
* Example:
 +
*
 +
* public Plugin myinfo =
 +
* {
 +
*    name = "My Plugin",
 +
*    //etc
 +
* };
 +
*/
 +
public Plugin myinfo;</sourcepawn>
 +
 
 +
It tells us that we need to create a global public variable <tt>myinfo</tt> which must be of type <tt>Plugin</tt> which is a struct with 5 fields which themselves are strings. It may sound complicated for a beginner but it's easy. Let's go ahead and create one:
 +
<sourcepawn>public Plugin myinfo =
 +
{
 +
name = "My First Plugin",
 +
author = "Me",
 +
description = "My first plugin ever",
 +
version = "1.0",
 +
url = "http://www.sourcemod.net/"
 +
};</sourcepawn>
 +
 
 +
The <tt>public</tt> keyword means that SourceMod will be able to directly access our variable. <tt>Plugin</tt> defines a type of our variable. <tt>myinfo</tt> is, obviously, a name of our variable as required by SourceMod. You see that we initialize it right away. This is the preferable way to fill out plugin info.
 +
 
 +
After that the full code of your plugin should look like this:
 +
<sourcepawn>#include <sourcemod>
 +
 
 +
public Plugin myinfo =
 +
{
 +
name = "My First Plugin",
 +
author = "Me",
 +
description = "My first plugin ever",
 +
version = "1.0",
 +
url = "http://www.sourcemod.net/"
 +
};</sourcepawn>
 +
 
 +
=Getting code to run=
 +
We already include SourceMod features and filled up or plugin info. We now have a perfectly well-formed plugin which can be compiled and loaded by SourceMod. However, there is one problem - it does nothing. You might be tempted to just start writing a code after <tt>myinfo</tt> declaration just to see that it will not compile. SourcePawn, unlike other scripting languages like Lua, does not allow a code to be outside of functions. After reading that, you may probably want to just define some function, name it <tt>main</tt> probably, compile and load a plugin and see that your code never gets called. So how do we make SourceMod call our code? For this exact reason, we have forwards. Forwards are function prototypes declared by one party that can be implemented by another party as a [http://en.wikipedia.org/wiki/Callback_%28computer_programming%29 callback]. When a first party starts a forward call, all parties that have matching callbacks receive the call. SourceMod declares a plenty of interesting forwards that we can implement. As you can see, forwards are the only way to get our code executed, keep that in mind. So let's implement <tt>OnPluginStart</tt> forward. As you may have guessed, it is called when our plugin starts. To do that, we'll have to look up the declaration of <tt>OnPluginStart</tt>. It is declared inside '''sourcemod.inc''', a file we are already familiar with, let's find it:
 +
<sourcepawn>/**
 +
* Called when the plugin is fully initialized and all known external references
 +
* are resolved. This is only called once in the lifetime of the plugin, and is
 +
* paired with OnPluginEnd().
 +
*
 +
* If any run-time error is thrown during this callback, the plugin will be marked
 +
* as failed.
 +
*
 +
* It is not necessary to close any handles or remove hooks in this function. 
 +
* SourceMod guarantees that plugin shutdown automatically and correctly releases
 +
* all resources.
 +
*
 +
* @noreturn
 +
*/
 +
forward void OnPluginStart();</sourcepawn>
 +
Empty parentheses tell us that no arguments are passed inside this forward, <tt>@noreturn</tt> inside documentation tells us that we don't have to return anything, pretty simple forward. So how to write a correct callback for it? Firstly, our callback must have the same name, so it's <tt>OnPluginStart</tt>, secondly, our callback should have the same number of arguments, none in this case, and lastly, SourceMod needs to be able to call our callback so it needs to be <tt>public</tt>. So the implementation looks like this:
 +
<sourcepawn>public void OnPluginStart()
 +
{
 +
}</sourcepawn>
 +
 
 +
Now we can write code inside curly braces and it will be executed when our plugin starts. Let's output <tt>"Hello world!"</tt> to server console. To do that we are going to use <tt>PrintToServer</tt> function. It is declared inside '''console.inc''', however, we don't need to manually include '''console.inc''' because it is included automatically as part of '''sourcemod.inc'''.
 +
<sourcepawn>/**
 +
* Sends a message to the server console.
 +
*
 +
* @param format Formatting rules.
 +
* @param ... Variable number of format parameters.
 +
* @noreturn
 +
*/
 +
native int PrintToServer(const char[] format, any ...);</sourcepawn>
 +
As you can see, this is a native function. It is implemented inside SM core. Judging by it's arguments, we can see that it is a [[Format_Class_Functions_%28SourceMod_Scripting%29|format class function]]. However, we don't need any formatting right now, so let's just pass <tt>"Hello world!"</tt> string as an only argument:
 +
<sourcepawn>public void OnPluginStart()
 +
{
 +
PrintToServer("Hello world!");
 +
}</sourcepawn>
 +
That's it! The full code of your plugin should look like this:
 +
<sourcepawn>#include <sourcemod>
  
public Plugin:myinfo =
+
public Plugin myinfo =
 
{
 
{
name = "Мой первый плагин",
+
name = "My First Plugin",
author = "Я",
+
author = "Me",
description = "Мой первый супер плагин",
+
description = "My first plugin ever",
version = "1.0.0.0",
+
version = "1.0",
 
url = "http://www.sourcemod.net/"
 
url = "http://www.sourcemod.net/"
 
};
 
};
  
public OnPluginStart()
+
public void OnPluginStart()
 
{
 
{
// Выполнение единовременного запуска задач ...
+
PrintToServer("Hello world!");
}</pawn>
+
}</sourcepawn>
 +
Compile and load your plugin on your server and see for yourself that the message is displayed in the server console.
  
Информация часть имеет специальную синтаксическую конструкцию. Вы не можете изменить любое из ключевых слов, или объявление <tt>public Plugin:myinfo</tt>. Хорошая идея заключается в том, чтобы скопировать и вставить эту скелетную структуру и отредактировать строки, чтобы начать работу.
 
  
 
=Включения=
 
=Включения=

Revision as of 12:14, 8 February 2021

Это руководство даст Вам основные понятия о написании SourceMod плагинов. Если вы не знакомы с языком SourcePawn, то Мы настоятельно рекомендуем ознакомиться со статьей Ru:Introduction to SourcePawn (Введение в SourcePawn).

Для получения информации о компиляции плагинов см. Compiling SourceMod Plugins (Компиляция SourceMod плагинов). Автор настоящей статьи использует редактор Crimson Editor для написания плагинов. Вы можете использовать PSPad, UltraEdit, Notepad++, TextPad, SourceMod IDE или любой другой текстовый редактор.

Начиная с нуля

Откройте Ваш любимый текстовый редактор и создайте новый пустой файл. Теперь Вы можете начать писать код, используя основные функции языка, однако, Вы не можете использовать любые функции и особенности самого SourceMod, потому что компилятор ничего не знает о них. Это сделано специально, чтобы было возможно использовать SourcePawn отдельно от SourceMod. Но так как мы пишем плагин именно для SourceMod, самая лучшая идея - это в первую очередь получить доступ к оособенностям SourceMod. Этой цели можно достичь с помощью директивы #include. Она сообщает компилятору, что содержимое указанного файла нужно вставить в Ваш при компиляции.

#include <sourcemod>

Как это работает? Сначала обратите внимание, что имя файла мы обрамили в угловые скобки. Угловые скобки сообщают компилятору, что указанный файл нужно поискать в стандартных директориях подключаемых файлов. По-умолчанию, такая директория одна, и это scripting/include. Вы можете открыть эту папку и увидеть множество файлов с расширением .inc здесь. Всё это - подключаемые файлы SourceMod, которые включают в себя описание множества функций, тегов и других особенностей, доступных плагинам SourceMod. Подключаемые файлы, по сути, это такой же обычный текст, как и сам код плагинов, потому Вы можете без проблем открыть его любым текстовым редактором и прочитать. Вы можете обратить внимание, однако, что здесь не так много кода, чтобы эти функции SourceMod вообще работали, так как же оно работает? Они реализованы внутри ядра самого SourceMod и написаны на C++, и скомпилированы в двоичные исполняемые файлы, которые размещаются в папке bin. Так как SourcePawn-код и SourceMod-код связываются между собой, если компилятор ничего не знает о существовании последнего? Подключаемые файлы SourceMod реализованы таким образом, чтобы компилятор понимал, что реализация функций будет где-то в другом месте. Компилятор понимает это, и генерирует специальный код, который вызовет внешнюю функцию. Когда SourceMod загружает Ваш плагин, он анализирует этот код и заменяет на вызов своих внутренних функций. Это называется динамическим связыванием.

Setting up plugin info

Now that we got access to SourceMod features, it is time to set up the information that will be displayed via sm plugins list command. No one likes unnamed plugins. To do that we are going to look inside sourcemod.inc file and see the format that information should be declared. It's always helpful to look inside SM include files to find out information you don't know. There is also an API documentation but it can be outdated and it only has SM core files so if your plugin is going to use any third party extension or another plugin, you will have to study inc files. So, open sourcemod.inc and scroll down a bit until you see this:

/**
 * Plugin public information.
 */
struct Plugin
{
   public const char[] name;		/**< Plugin Name */
   public const char[] description;	/**< Plugin Description */
   public const char[] author;		/**< Plugin Author */
   public const char[] version;		/**< Plugin Version */
   public const char[] url;			/**< Plugin URL */
};

and this:

/**
 * Declare this as a struct in your plugin to expose its information.
 * Example:
 *
 * public Plugin myinfo =
 * {
 *    name = "My Plugin",
 *    //etc
 * };
 */
public Plugin myinfo;

It tells us that we need to create a global public variable myinfo which must be of type Plugin which is a struct with 5 fields which themselves are strings. It may sound complicated for a beginner but it's easy. Let's go ahead and create one:

public Plugin myinfo =
{
	name = "My First Plugin",
	author = "Me",
	description = "My first plugin ever",
	version = "1.0",
	url = "http://www.sourcemod.net/"
};

The public keyword means that SourceMod will be able to directly access our variable. Plugin defines a type of our variable. myinfo is, obviously, a name of our variable as required by SourceMod. You see that we initialize it right away. This is the preferable way to fill out plugin info.

After that the full code of your plugin should look like this:

#include <sourcemod>
 
public Plugin myinfo =
{
	name = "My First Plugin",
	author = "Me",
	description = "My first plugin ever",
	version = "1.0",
	url = "http://www.sourcemod.net/"
};

Getting code to run

We already include SourceMod features and filled up or plugin info. We now have a perfectly well-formed plugin which can be compiled and loaded by SourceMod. However, there is one problem - it does nothing. You might be tempted to just start writing a code after myinfo declaration just to see that it will not compile. SourcePawn, unlike other scripting languages like Lua, does not allow a code to be outside of functions. After reading that, you may probably want to just define some function, name it main probably, compile and load a plugin and see that your code never gets called. So how do we make SourceMod call our code? For this exact reason, we have forwards. Forwards are function prototypes declared by one party that can be implemented by another party as a callback. When a first party starts a forward call, all parties that have matching callbacks receive the call. SourceMod declares a plenty of interesting forwards that we can implement. As you can see, forwards are the only way to get our code executed, keep that in mind. So let's implement OnPluginStart forward. As you may have guessed, it is called when our plugin starts. To do that, we'll have to look up the declaration of OnPluginStart. It is declared inside sourcemod.inc, a file we are already familiar with, let's find it:

/**
 * Called when the plugin is fully initialized and all known external references 
 * are resolved. This is only called once in the lifetime of the plugin, and is 
 * paired with OnPluginEnd().
 *
 * If any run-time error is thrown during this callback, the plugin will be marked 
 * as failed.
 *
 * It is not necessary to close any handles or remove hooks in this function.  
 * SourceMod guarantees that plugin shutdown automatically and correctly releases 
 * all resources.
 *
 * @noreturn
 */
forward void OnPluginStart();

Empty parentheses tell us that no arguments are passed inside this forward, @noreturn inside documentation tells us that we don't have to return anything, pretty simple forward. So how to write a correct callback for it? Firstly, our callback must have the same name, so it's OnPluginStart, secondly, our callback should have the same number of arguments, none in this case, and lastly, SourceMod needs to be able to call our callback so it needs to be public. So the implementation looks like this:

public void OnPluginStart()
{
}

Now we can write code inside curly braces and it will be executed when our plugin starts. Let's output "Hello world!" to server console. To do that we are going to use PrintToServer function. It is declared inside console.inc, however, we don't need to manually include console.inc because it is included automatically as part of sourcemod.inc.

/**
 * Sends a message to the server console.
 *
 * @param format		Formatting rules.
 * @param ...			Variable number of format parameters.
 * @noreturn
 */
native int PrintToServer(const char[] format, any ...);

As you can see, this is a native function. It is implemented inside SM core. Judging by it's arguments, we can see that it is a format class function. However, we don't need any formatting right now, so let's just pass "Hello world!" string as an only argument:

public void OnPluginStart()
{
	PrintToServer("Hello world!");
}

That's it! The full code of your plugin should look like this:

#include <sourcemod>
 
public Plugin myinfo =
{
	name = "My First Plugin",
	author = "Me",
	description = "My first plugin ever",
	version = "1.0",
	url = "http://www.sourcemod.net/"
};
 
public void OnPluginStart()
{
	PrintToServer("Hello world!");
}

Compile and load your plugin on your server and see for yourself that the message is displayed in the server console.


Включения

Pawn требует включать файлы include, как и C требует наличие заголовка у файла. Включение списков файлов всех структур, функций, вызовы и тегов, которые имеются в наличии. Есть три типа файлов:

  • Core - sourcemod.inc, и все его включения. Все представленные в SourceMod Core.
  • Extension - добавляет зависимость от определенного расширения.
  • Plugin - добавляет зависимость от некоторых плагинов.

Файлы включения загружаются с помощью #include указателя компилятора.

Команды

В Нашем первом примере будет написана простая команда для администратора, чтобы ударить игрока. Мы будем расширять функциональность этого примера, пока не получим окончательный результат, максимально полным.

Описание

Во-первых, давайте смотреть на то, какая команда требуется администратору. Команды администратора регистрируются с использованием функции RegAdminCmd. Она требует название, функцию обратного вызова и флаги админа по умолчанию.

Функция обратного вызова это то, на что ссылаться используемая команда каждый раз. Нажми здесь, чтобы просмотреть её прототип. Пример:

public OnPluginStart()
{
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY)
}
 
public Action:Command_MySlap(client, args)
{
}

Теперь Мы успешно обеспечили выполнение команды -- хотя она не будет ничего делать. На самом деле, она скажет "Неизвестная команда", если Вы используете её! Причина в том, что отсутствует Action тег. По умолчанию функция ввода консольных команд заключается в том, чтобы ответить о неизвестной команде. Чтобы заблокировать эту функцию, вы должны создать новое действие:

public Action:Command_MySlap(client, args)
{
	return Plugin_Handled;
}

Теперь команда не будет сообщать об ошибке, но она по-прежнему не будет ничего делать.

Реализация

Давайте решим, что команда будет выглядеть так. Пусть будет она действовать как команда по умолчанию sm_slap:

sm_myslap <name|#userid> [damage]

Чтобы осуществить это, Нам потребуется несколько шагов:

  • Получить ввод с консоли. Для этого мы используем GetCmdArg().
  • Найти соответствия игрока. Для этого мы используем FindTarget().
  • Ударить его. Для этого мы используем SlapPlayer(), которая требует в том числе sdktools расширение в комплекте с SourceMod.
  • Сообщить администратору. Для этого мы используем ReplyToCommand().

Полный пример:

#include <sourcemod>
#include <sdktools>
 
public Plugin:myinfo =
{
	name = "Мой первый плагин",
	author = "Я",
	description = "Мой первый супер плагин",
	version = "1.0.0.0",
	url = "http://www.sourcemod.net/"
}
 
public OnPluginStart()
{
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY)
}
 
public Action:Command_MySlap(client, args)
{
	new String:arg1[32], String:arg2[32]
	new damage
 
	/* Получаем первый аргумент */
	GetCmdArg(1, arg1, sizeof(arg1))
 
	/* Если есть 2 или более аргументов, и второй аргумент получен
	 * успешно, превратить его в целое.
	 */
	if (args >= 2 && GetCmdArg(2, arg2, sizeof(arg2)))
	{
		damage = StringToInt(arg2)
	}
 
	/* Попытка и нахождение соответствия игрока */
	new target = FindTarget(client, arg1)
	if (target == -1)
	{
		/* FindTarget() автоматически отвечает с
		 * причиной провала.
		 */
		return Plugin_Handled;
	}
 
	SlapPlayer(target, damage)
 
	new String:name[MAX_NAME_LENGTH]
 
	GetClientName(target, name, sizeof(name))
	ReplyToCommand(client, "[SM] Вас ударил %s на %d повреждений!", name, damage)
 
	return Plugin_Handled;
}

Для получения дополнительной информации о том, что такое %s и %d, см. Ru:Format Class Functions. Имейте в виду, что Вам никогда не придется отменять или удалить Ваши команды администратора. Если плагин выгружается, SourceMod очищает их за Вас.

ConVars

ConVars, известные также как cvars, глобальные консольные переменные в движке Source. Они могут иметь целые, десятичные, или строковые значения. Доступ к ConVar осуществляется через дескрипторы (Handles). С тех пор как ConVars имеют глобальный характер, Вам не нужно закрывать дескрипторы ConVar (фактически, Вы и не можете).

Удобная особенность ConVars заключается в том, что они легко настраиваются пользователями. Они могут быть помещены в любой .cfg файл, например server.cfg или sourcemod.cfg. Чтобы сделать удобнее их использование, SourceMod имеет AutoExecConfig() функции. Эта функция автоматически создает .cfg файл по умолчанию, содержащий все Ваши переменные (cvars), снабженные комментариями для пользователей. Очень рекомендую Вам вызывать её, если у Вас есть настраиваемые ConVars.

Давайте усовершенствуем Наш предыдущий пример новой ConVar. ConVar назовём sm_myslap_damage и она будет определять повреждение по умолчанию для удара игрока, если размер повреждений не указан.

new Handle:sm_myslap_damage = INVALID_HANDLE
 
public OnPluginStart()
{
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY)
 
	sm_myslap_damage = CreateConVar("sm_myslap_damage", "5", "Повреждение от удара по умолчанию")
	AutoExecConfig(true, "plugin_myslap")
}
 
public Action:Command_MySlap(client, args)
{
	new String:arg1[32], String:arg2[32]
	new damage = GetConVarInt(sm_myslap_damage)
 
	/* Остальное остается без изменений! */

Отображение активности, логирование

Почти все команды администратора должны логировать (записывать в лог) их активность, а некоторые команды администратора должны отобразить свою активность в игре для клиентов. Это может быть сделано через LogAction() и ShowActivity2() функции. Точное функциональность ShowActivity2() определяется sm_show_activity переменной.

Например, давайте перепишем несколько последних строк нашей slap команды:

	SlapPlayer(target, damage)
 
	new String:name[MAX_NAME_LENGTH]
 
	GetClientName(target, name, sizeof(name))
 
	ShowActivity2(client, "[SM] ", "Вас ударил %s на %d повреждений!", name, damage)
	LogAction(client, target, "\"%L\" slapped \"%L\" (damage %d)", client, target, damage)
 
	return Plugin_Handled;
}

Групповые цели

Чтобы полностью завершить нашу демонстрацию slap, давайте сделаем поддержку нескольких целей. Targeting system в SourceMod достаточно усовершенствована, её использование может показаться сложным на первый взгляд.

Мы используем функцию ProcessTargetString(). Он принимает ввод с консоли, и возвращает список подходящих клиентов. Она также возвращает существительное, которые будет определять либо одного клиента или характеризовать список клиентов. Идея заключается в том, что каждый клиент будет обработан, но активность будет показана всем игрокам только один раз. Это уменьшит спам на экране.

Этот метод обработки цели используется почти каждой командой администратора в SourceMod, а на самом деле FindTarget() является лишь упрощенной версией.

Полный, окончательный пример:

#include <sourcemod>
#include <sdktools>
 
new Handle:sm_myslap_damage = INVALID_HANDLE
 
public Plugin:myinfo =
{
	name = "Мой первый плагин",
	author = "Я",
	description = "Мой первый супер плагин",
	version = "1.0.0.0",
	url = "http://www.sourcemod.net/"
}
 
public OnPluginStart()
{
	LoadTranslations("common.phrases")
	RegAdminCmd("sm_myslap", Command_MySlap, ADMFLAG_SLAY)
 
	sm_myslap_damage = CreateConVar("sm_myslap_damage", "5", "Повреждение от удара по умолчанию")
	AutoExecConfig(true, "plugin_myslap")
}
 
public Action:Command_MySlap(client, args)
{
	new String:arg1[32], String:arg2[32]
	new damage = GetConVarInt(sm_myslap_damage)
 
	/* Получаем первый аргумент */
	GetCmdArg(1, arg1, sizeof(arg1))
 
	/* Если есть 2 или более аргументов, и второй аргумент получен
	 * успешно, превратить его в целое.
	 */
	if (args >= 2 && GetCmdArg(2, arg2, sizeof(arg2)))
	{
		damage = StringToInt(arg2)
	}
 
	/**
	 * target_name - сохраняет существительные установленной цели(ей)
	 * target_list - массив для хранения клиентов
	 * target_count - переменная для хранения числа клиентов
	 * tn_is_ml - сохраняет должно ли существительное быть переведено
	 */
	new String:target_name[MAX_TARGET_LENGTH]
	new target_list[MAXPLAYERS], target_count
	new bool:tn_is_ml
 
	if ((target_count = ProcessTargetString(
			arg1,
			client,
			target_list,
			MAXPLAYERS,
			COMMAND_FILTER_ALIVE, /* Разрешено только живым игрокам */
			target_name,
			sizeof(target_name),
			tn_is_ml)) <= 0)
	{
		/* Эта функция отвечает администратору сообщение о неудачи */
		ReplyToTargetError(client, target_count);
		return Plugin_Handled;
	}
 
	for (new i = 0; i < target_count; i++)
	{
		SlapPlayer(target_list[i], damage)
		LogAction(client, target_list[i], "\"%L\" slapped \"%L\" (damage %d)", client, target_list[i], damage)
	}
 
	if (tn_is_ml)
	{
		ShowActivity2(client, "[SM] ", "Slapped %t for %d damage!", target_name, damage)
	}
	else
	{
		ShowActivity2(client, "[SM] ", "Slapped %s for %d damage!", target_name, damage)
	}
 
	return Plugin_Handled;
}

Клиентский и объектный индексы

Одним основным вопросом путаницы с Half-Life 2 является разница между следующими вещами:

  • Индекс клиента (Client index)
  • Индекс объекта (Entity index)
  • Идентификатор пользователя (Userid)

Первый ответ заключается в том, что клиенты это объекты. Таким образом, индекс клиента и индекс объекта одно и тоже. Когда SourceMod функция запрашивает индекс объекта, индекс клиента может быть указан. Когда SourceMod функция запрашивает индекс клиента, как правило, это означает, что только индекс клиента может быть указан.

Быстрый способ проверить является ли индекс объекта клиентом - проверить находится ли он между 1 и GetMaxClients() (включительно). Если сервер имеет максимум N клиентских слотов, то объекты с 1 по N, всегда зарезервированы для клиентов. Заметим, что 0 - верный индекс объекта; он является общим объектом (worldspawn).

Идентификатор пользователя, это абсолютно другое. Сервер поддерживает общее "число соединений", и оно начинается с 1. Каждый раз, когда подключается новый клиент, общее число соединений увеличивается, и клиент получает новый номер, называемый идентификатором пользователя.

Например, первый подключившийся клиент имеет идентификатор пользователя 2. Если он вышел и соединился заново, его идентификатор будет 3 (если другие клиенты не подключились в этот период). Since clients are disconnected on mapchange, their userids change as well. Userids are a handy way to check if a client's connection status has changed.

SourceMod предусматривает две функции для userid: GetClientOfUserId() и GetClientUserId().

События

События являются информационными сообщениями передаваемые между объектами на сервере. Многие так же передаются от сервера к клиенту. Они определены в .res файлах в папке hl2/resource и папке resource конкретных модов. Основной список смотрите здесь Source Game Events.

Важно отметить некоторые особенности событий:

  • Они почти всегда носят информационных характер. Т.е., блокирование player_death не остановит смерть игрока. Он может заблокировать HUD или консольное сообщение или что то незначительное.
  • Они всегда используют userid вместо индексов клиента(client indexes).
  • То что они находятся в файле ресурсов не означает что они когда-либо будут вызываться или работать так как вы ожидаете. Моды, как известно, не должным образом документируют функционирование своих событий.


Пример обнаружения, когда игрок умирает:

public OnPluginStart()
{
   HookEvent("player_death", Event_PlayerDeath)
}
 
public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast)
{
   new victim_id = GetEventInt(event, "userid")
   new attacker_id = GetEventInt(event, "attacker")
 
   new victim = GetClientOfUserId(victim_id)
   new attacker = GetClientOfUserId(attacker_id)
 
   /* CODE */
}

Callback Orders and Pairing

SourceMod has a number of builtin callbacks about the state of the server and plugin. Some of these are paired in special ways which is confusing to users.

Pairing

Pairing is SourceMod terminology. Examples of it are:

  • OnMapEnd() cannot be called without an OnMapStart(), and if OnMapStart() is called, it cannot be called again without an OnMapEnd().
  • OnClientConnected(N) for a given client N will only be called once, until an OnClientDisconnected(N) for the same client N is called (which is guaranteed to happen).

There is a formal definition of SourceMod's pairing. For two functions X and Y, both with input A, the following conditions hold:

  • If X is invoked with input A, it cannot be invoked again with the same input unless Y is called with input A.
  • If X is invoked with input A, it is guaranteed that Y will, at some point, be called with input A.
  • Y cannot be invoked with any input A unless X was called first with input A.
  • The relationship is described as, "X is paired with Y," and "Y is paired to X."

General Callbacks

These callbacks are listed in the order they are called, in the lifetime of a plugin and the server.

  • AskPluginLoad() - Called once, immediately after the plugin is loaded from the disk.
  • OnPluginStart() - Called once, after the plugin has been fully initialized and can proceed to load. Any run-time errors in this function will cause the plugin to fail to load. This is paired with OnPluginEnd().
  • OnMapStart() - Called every time the map loads. If the plugin is loaded late, and the map has already started, this function is called anyway after load, in order to preserve pairing. This function is paired with OnMapEnd().
  • OnConfigsExecuted() - Called once per map-change after servercfgfile (usually server.cfg), sourcemod.cfg, and all plugin config files have finished executing. If a plugin is loaded after this has happened, the callback is called anyway, in order to preserve pairing. This function is paired with OnMapEnd().
  • At this point, most game callbacks can occur, such as events and callbacks involving clients (or other things, like OnGameFrame).
  • OnMapEnd() - Called when the map is about to end. At this point, all clients are disconnected, but TIMER_NO_MAPCHANGE timers are not yet destroyed. This function is paired to OnMapStart().
  • OnPluginEnd() - Called once, immediately before the plugin is unloaded. This function is paired to OnPluginStart().

Client Callbacks

These callbacks are listed in no specific order, however, their documentation holds for both fake and real clients.

  • OnClientConnect() - Called when a player initiates a connection. This is paired with OnClientDisconnect() for successful connections only.
  • OnClientAuthorized() - Called when a player gets a Steam ID. It is important to note that this may never be called. It may occur any time in between connect and disconnect. Do not rely on it unless you are writing something that needs Steam IDs, and even then you should use OnClientPostAdminCheck().
  • OnClientPutInServer() - Signifies that the player is in-game and IsClientInGame() will return true.
  • OnClientPostAdminCheck() - Called after the player is both authorized and in-game. That is, both OnClientAuthorized() and OnClientPutInServer() have been invoked. This is the best callback for checking administrative access after connect.
  • OnClientDisconnect() - Called when a player's disconnection ends. This is paired to OnClientConnect().

Frequently Asked Questions

Are plugins reloaded every mapchange?

Plugins, by default, are not reloaded on mapchange unless their timestamp changes. This is a feature so plugin authors have more flexibility with the state of their plugins.

Do I need to call CloseHandle in OnPluginEnd?

No. SourceMod automatically closes your Handles when your plugin is unloaded, in order to prevent memory errors.

Do I need to #include every individual .inc?

No. #include <sourcemod> will give you 95% of the .incs. Similarly, #include <sdktools> includes everything starting with <sdktools>.

Why don't some events fire?

There is no guarantee that events will fire. The event listing is not a specification, it is a list of the events that a game is capable of firing. Whether the game actually fires them is up to Valve or the developer.

Do I need to CloseHandle timers?

No. In fact, doing so may cause errors. Timers naturally die on their own unless they are infinite timers, in which case you can use KillTimer() or die gracefully by returning Plugin_Stop in the callback.

Are clients disconnected on mapchange?

All clients are fully disconnected before the map changes. They are all reconnected after the next map starts.


Продолжение обучения

Для дальнейшего обучения, смотри раздел "Scripting" в Ru:SourceMod Documentation.