Ru:Introduction to SourceMod Plugins

From AlliedModders Wiki
Revision as of 05:54, 30 December 2008 by Frenzzy (talk | contribs) (ConVars)
Jump to: navigation, search

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

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

Структура плагина

Почти все плагины имеют три одинаковых элемента:

  • Includes - Позволяет получить доступ к SourceMod API, и если Вы хотите, к API от внешних SourceMod плагинов и расширений.
  • Info - Общественная информация о Вашем плагине.
  • Startup - Функция, которая осуществляет запуск процедур в Вашем плагине.

Плагин скелетной структуры выглядит следующим образом:

#include <sourcemod>
 
public Plugin:myinfo =
{
	name = "Мой первый плагин",
	author = "Я",
	description = "Мой первый супер плагин",
	version = "1.0.0.0",
	url = "http://www.sourcemod.net/"
};
 
public OnPluginStart()
{
	// Выполнение единовременного запуска задач ...
}

Информация часть имеет специальную синтаксическую конструкцию. Вы не можете изменить любое из ключевых слов, или объявление public Plugin:myinfo. Хорошая идея заключается в том, чтобы скопировать и вставить эту скелетную структуру и отредактировать строки, чтобы начать работу.

Включения

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;
}

Multiple Targets

To fully complete our slap demonstration, let's make it support multiple targets. SourceMod's targeting system is quite advanced, so using it may seem complicated at first.

The function we use is ProcessTargetString(). It takes in input from the console, and returns a list of matching clients. It also returns a noun that will identify either a single client or describe a list of clients. The idea is that each client is then processed, but the activity shown to all players is only processed once. This reduces screen spam.

This method of target processing is used for almost every admin command in SourceMod, and in fact FindTarget() is just a simplified version.

Full, final example:

#include <sourcemod>
#include <sdktools>
 
new Handle:sm_myslap_damage = INVALID_HANDLE
 
public Plugin:myinfo =
{
	name = "My First Plugin",
	author = "Me",
	description = "My first plugin ever",
	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", "Default slap damage")
	AutoExecConfig(true, "plugin_myslap")
}
 
public Action:Command_MySlap(client, args)
{
	new String:arg1[32], String:arg2[32]
	new damage = GetConVarInt(sm_myslap_damage)
 
	/* Get the first argument */
	GetCmdArg(1, arg1, sizeof(arg1))
 
	/* If there are 2 or more arguments, and the second argument fetch 
	 * is successful, convert it to an integer.
	 */
	if (args >= 2 && GetCmdArg(2, arg2, sizeof(arg2)))
	{
		damage = StringToInt(arg2)
	}
 
	/**
	 * target_name - stores the noun identifying the target(s)
	 * target_list - array to store clients
	 * target_count - variable to store number of clients
	 * tn_is_ml - stores whether the noun must be translated
	 */
	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, /* Only allow alive players */
			target_name,
			sizeof(target_name),
			tn_is_ml)) <= 0)
	{
		/* This function replies to the admin with a failure message */
		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;
}

Client and Entity Indexes

One major point of confusion with Half-Life 2 is the difference between the following things:

  • Client index
  • Entity index
  • Userid

The first answer is that clients are entities. Thus, a client index and an entity index are the same thing. When a SourceMod function asks for an entity index, a client index can be specified. When a SourceMod function asks for a client index, usually it means only a client index can be specified.

A fast way to check if an entity index is a client is checking whether it's between 1 and GetMaxClients() (inclusive). If a server has N client slots maximum, then entities 1 through N are always reserved for clients. Note that 0 is a valid entity index; it is the world entity (worldspawn).

A userid, on the other hand, is completely different. The server maintains a global "connection count" number, and it starts at 1. Each time a client connects, the connection count is incremented, and the client receives that new number as their userid.

For example, the first client to connect has a userid of 2. If he exits and rejoins, his userid will be 3 (unless another client joins in-between). 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 provides two functions for userids: GetClientOfUserId() and GetClientUserId().

Events

Events are informational notification messages passed between objects in the server. Many are also passed from the server to the client. They are defined in .res files under the hl2/resource folder and resource folders of specific mods. For a basic listing, see Source Game Events.

It is important to note a few concepts about events:

  • They are almost always informational. That is, blocking player_death will not stop a player from dying. It may block a HUD or console message or something else minor.
  • They always use userids instead of client indexes.
  • Just because it is in a resource file does not mean it is ever called, or works the way you expect it to. Mods are notorious at not properly documenting their event functionality.

An example of finding when a player dies:

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.


Further Reading

For further reading, see the "Scripting" section at the SourceMod Documentation.