[завершено][SourcePawn] Советы для новичков и профи

Статус
В этой теме нельзя размещать новые ответы.

Dragokas

Angry & Scary Developer
Команда форума
Супер-Модератор
Разработчик
Клуб переводчиков
Сообщения
8,030
Решения
14
Реакции
6,805
Автор: SilverShot
Перевод и техническая редакция: Dragokas
Дополнения: разработчики и сообщество SourceMod
Источник:
AM
Перепубликация перевода на другие ресурсы запрещена!

Оригинал перевода статьи размещён на форуме hlmod.ru в этой теме. Пожалуйтса, оставляйте свои комментарии там.

Пожалуйста, внесите свой вклад в наполнение этой темы, советуя изменения, ссылки и другую информацию.

Ремарка от переводчика:
На перевод меня сподвигло желание донести до русскоязычной аудитории эту замечательную статью от SilverShot.
Автор имеет 10-летний опыт скриптинга для игр, в основном серии Left 4 Dead, и более 100 опубликованных работ.

К сожалению, маловероятно, что на AM статья будет обновляться, т.к. автор объявил о своём уходе со сцены.
Вероятно, я также перестану заниматься SourcePawn, т.к. мой сервер L4D1, некогда попадавший в TOP-1 мира по версии GameTracker, сейчас переживает не лучшие времена со значительным оттоком игроков, впрочем как и у всех, так что это может быть моим прощальным подарком сообществу. Я обещаю поддерживать тему какое-то время.
Будет круто добавить сюда лучшие практики из CS:GO и др. популярных игр. Давайте создадим отличную базу знаний общими усилиями!

Примечание: в статье есть множество отсылок на англоязычные источники. Их перевод мною не планируется. Однако, если вы нашли русскоязычный аналог, просьба сообщить в ЛС и я заменю ссылку.

Содержание:
Дополнения:​



Терминология:
Инфа от переводчика:​
Чтобы рядовой читатель не сбился среди множества новых понятий, я решил сделать вводную о взаимозаменяемости терминов и сокращений, которые используются далее в переводе.​
Напишите в комментариях, если вам требуются пояснения для других терминов.​
ТерминПеревод (аналоги)Описание
Scriptскрипт, скриптинг, кодингфайл с исходным кодом и сам процесс его написания
Cvar, ConVar (Console Variable)квар, переменная консолиСуществуют как собственные квары игры, так и создаваемые через SourceMod.
Handleхендл, дескрипторчисло, указывающее на системный объект внутри SM, через которое можно им манипулировать. Большинство других видов объектов - это производные от Handle (например, ConVar)
Entityэнтитя, сущностьвнутри-игровой объект, которым можно манипулировать через его индекс. Пример - клиент (игрок, CTerrorPlayer).
Propпроп. Применяется в 2 значениях:
- как свойство сущности (пример, - цвет ("color") - это имя свойства; Prop_Send / Prop_Data - типы свойств)
- как один из классов сущности (например, prop_dynamic). От класса зависит поведение сущности, набор доступных в ней свойств и пр.
Particleпартикль, частицаПример - огонь.
UserMessagesпользовательские сообщенияособый тип сообщений передаваемых по сети клиенту. Пример - сообщения чата
TempEntsвременные сущностипохожие по принципу с UserMessages, но предназначенные для графической составляющей, например, следы от пуль. Чем-то схожи с партиклями.
Spawnспаун, спавнитьсяпоявляться, создаваться
Patchпатчизменение в памяти процесса
Hookхук, хукатьставить прослушку на событие с дальнейшей возможностью (или без) его изменения
Detourдетур, обход, перехваттоже самое, только для функций игры. В отличие от хука, не предусмотрен игрой, и обычно ведёт к выполнению вашего кода вместо функции игры, с возможностью или без её продолжения
Callbackколбек, процедура обратного вызовафункция или процедура в вашем коде, указатель на которую вы передали. Она вызывается каждый раз при наступлении события / срабатывании условия.
Listen Server"слушающий" серверустановленный поверх вашей же игры
Local Dedicated Serverлокальный выделенный серверсервер на вашем ПК, установленный отдельно от файлов игрового клиента
Server Crashкреш, падениесерьезный сбой в функциях игры, ведущий к закрытию (и перезагрузке) сервера. Исходный процесс завершается.
Client Crashтоже самое, но для игрока (игрового клиента). При этом, сервер не обязательно падает.
Nativeнативчитайте далее по теме
Forwardфорвардчитайте далее по теме
ParserПарсеркод, реализующий разбор сложной структуры данных на отдельные компоненты
Init, InitializationИнициализацияначальная стадия, при которой объект получает все необходимые для его первичной работы свойства.
CompilationКомпиляцияпроцесс преобразования исходного кода (например, sp) в исполняемый код (smx) с инструкциями или псевдо-кодом, понятными для процессора либо транслирующей его промежуточной программы.
Validationпроверка, валидация, верификацияопределение объекта как такового, что является действительным, т.е. он существует и доступен для выполнения с ним некоторых действий.


1. Основы SourceMod и документация:
Я хочу подчеркнуть, на сколько сильно вам может помочь изучение большого числа различных плагинов и примеров при обучении языку.​
Используйте find для поиска кваров (cvar, консольных переменных) и команд, введя "find <ключевое слово>" в серверную или клиентскую консоль. Они могут возвращать разные результаты, например, квары специфические только для сервера или клиента.​
Новичкам в скриптинге:
Полезные ресурсы:
  • Scripting API Reference - Список всех функций, доступных в SourceMod.
  • Scripting API Reference - Тёмная тема.

  • SourceMod Development - "Эта категория содержит статьи о разработке расширений (extensions) для SourceMod" (продвинутый уровень)

  • Valve Wiki - Масса отличной информации, начиная от подробностей о сущностях (entities) вплоть до VScripts (CS:GO / L4D2).
    Нажмите на первую ссылку выше и выберите вашу конкретную игру.

  • Сторонние инструменты - например, распаковщики .VPK архивов, .nuc VScript декодеры, декомпиляторы .BSP карт в исходник и его запуск с помощью Hammer Editor - полезен, чтобы узнать, как карта выполняет те или иные действия.
    В CS:GO и L4D2 вам может захотеться воспользоваться плагином VScript File Replacer для декодирования и создания дампа VScripts.

  • Ещё больше сторонних инструментов - Перечень от Dragokas.

  • AlliedMods Cross-Reference - Различные SDK и исходный код AlliedMods для SourceMod и MetaMod.
  • HL2SDK - Для продвинутого исследования работы движка, там вы можете найти исходный код.

  • Plugin List - Поиск по всем плагинам.
Русскоязычные ресурсы:
 
Последнее редактирование:
2. Установка сервера:

Steam CMD:
  • SteamCMD используется для скачивания, установки и обновления серверов. Скачайте и установите SteamCMD прежде, чем устанавливать сервер. (Прим. переводчика: для Linux существует скрипт LinuxGSM, значительно облегчающий всю процедуру установки и управления сервером. Лучше всего работает под Debian 10 x64).

Пример Windows .bat файла для установки и обновления CS:GO сервера. Для других серверов просто поменяйте APP_ID на номер из этого списка.​
Установка и обновление CS:GO:
PHP:
@echo off

set "STEAM=C:\Servers\SteamCMD"
set "GAME_DIR=C:\Servers\CSGO"
set "APP_ID=740"

set "STEAM_USERNAME=anonymous"
set "STEAM_PASSWORD="

cd /d "%STEAM%"
start "" steamcmd.exe +login "%STEAM_USERNAME%" "%STEAM_PASSWORD%" +force_install_dir "%GAME_DIR%" +app_update %APP_ID% validate

Пример Windows .bat файла для запускa CS:GO сервера, который автоматически стартует на карте de_dust2.​
Запуск CS:GO:
PHP:
@echo off

set "GAME_DIR=C:\Servers\CSGO"
set "GAME_EXE=srcds.exe -debug -condebug -console -game csgo +sv_pure 1 +map de_dust2 +hostport 27016 +clientport 27006"

START %GAME_DIR%\%GAME_EXE%

Пример Windows .bat файла для запуска L4D2 сервера и отображения списка опций для выбора желаемой карты, с которой стартует сервер.​
Запуск L4D2:
PHP:
@echo off
SetLocal EnableExtensions

set "GAME_DIR=C:\Servers\L4D2\\"
set "GAME_EXE=srcds.exe -debug -condebug -console -game left4dead2 -netconport 27505 +hostport 27016 +clientport 27006 +exec server.cfg"

REM :MENU
echo.
echo.                  LEFT 4 DEAD 2  ---  DEDICATED SERVER LAUNCHER
echo. ##############################################################################
echo.
echo.                                1 - Dead Center
echo.                                2 - Dark Carnival
echo.                                3 - Swamp Fever
echo.                                4 - Hard Rain
echo.                                5 - The Parish
echo.                                6 - The Passing
echo.                                7 - The Sacrifice
echo.                                8 - No Mercy
echo.                                9 - Crash Course
echo.                                10 - Death Toll
echo.                                11 - Dead Air
echo.                                12 - Blood Harvest
echo.                                13 - Cold Stream
echo.
echo. ##############################################################################
echo.

set /P M=Select Map or Update:
echo.

IF %M%==1 START %GAME_DIR%%GAME_EXE% +map c1m1_hotel
IF %M%==2 START %GAME_DIR%%GAME_EXE% +map c2m1_highway
IF %M%==3 START %GAME_DIR%%GAME_EXE% +map c3m1_plankcountry
IF %M%==4 START %GAME_DIR%%GAME_EXE% +map c4m1_milltown_a
IF %M%==5 START %GAME_DIR%%GAME_EXE% +map c5m1_waterfront
IF %M%==6 START %GAME_DIR%%GAME_EXE% +map c6m1_riverbank
IF %M%==7 START %GAME_DIR%%GAME_EXE% +map c7m1_docks
IF %M%==8 START %GAME_DIR%%GAME_EXE% +map c8m1_apartment
IF %M%==9 START %GAME_DIR%%GAME_EXE% +map c9m1_alleys
IF %M%==10 START %GAME_DIR%%GAME_EXE% +map c10m1_caves
IF %M%==11 START %GAME_DIR%%GAME_EXE% +map c11m1_greenhouse
IF %M%==12 START %GAME_DIR%%GAME_EXE% +map c12m1_hilltop
IF %M%==13 START %GAME_DIR%%GAME_EXE% +map c13m1_alpinecreek

Экономия дискового пространства:

Я запускаю игровой клиент и сервер на одном и том же ПК. Для экономии места я использую Симлинки, которыми можно связать часть серверных файлов с клиентскими, заменив оригинал на "ярлыки".​

Это означает, что как только мой клиент обновляется, часть серверных файлов также обновляется. Убедитесь также, что обновили сервер. С обновлениями могут приходить новые файлы .VPK, так что вам может захотеться добавить симлинки и на их тоже.​

Похоже, здесь есть проблема при обновлении сервера. Он повторно загружает некоторые файлы, заменяя ими симлинки. Это не очень хорошо для игр, которые часто обновляются. Прим. переводчика: чтобы это не происходило, вы можете использовать хардлинки вместо симлинков. Просто добавьте ключ /H к каждой из указанных в коде ниже команд mklink. Будьте осторожны, если вам захочется удалить такой хардлинк, вы тем самым одновременно удалите и его источник, т.е. файл или папку на который он указывает. В Windows 7 и 8.1 батник необходимо запускать от имени Администратора.​

Примеры Windows .bat файлов, сэкономят ~14.8 ГБ. для CS:GO и ~4.81 ГБ + 10,200 файлов для L4D2:​
Симлинки для CS:GO:
PHP:
@echo off

REM Экономит ~14.8gb
REM Измените пути к папкам. Эти симлинки с серверной стороны будут указывать на клиентские файлы.
set "SERVER=C:\Servers\CSGO\csgo"
set "CLIENT=C:\Steam\SteamApps\common\Counter-Strike Global Offensive\csgo"

mklink "%SERVER%\pak01_000.vpk" "%CLIENT%\pak01_000.vpk"
mklink "%SERVER%\pak01_001.vpk" "%CLIENT%\pak01_001.vpk"
mklink "%SERVER%\pak01_002.vpk" "%CLIENT%\pak01_002.vpk"
mklink "%SERVER%\pak01_003.vpk" "%CLIENT%\pak01_003.vpk"
mklink "%SERVER%\pak01_004.vpk" "%CLIENT%\pak01_004.vpk"
mklink "%SERVER%\pak01_005.vpk" "%CLIENT%\pak01_005.vpk"
mklink "%SERVER%\pak01_006.vpk" "%CLIENT%\pak01_006.vpk"
mklink "%SERVER%\pak01_007.vpk" "%CLIENT%\pak01_007.vpk"
mklink "%SERVER%\pak01_008.vpk" "%CLIENT%\pak01_008.vpk"
mklink "%SERVER%\pak01_009.vpk" "%CLIENT%\pak01_009.vpk"
mklink "%SERVER%\pak01_010.vpk" "%CLIENT%\pak01_010.vpk"
mklink "%SERVER%\pak01_011.vpk" "%CLIENT%\pak01_011.vpk"
mklink "%SERVER%\pak01_012.vpk" "%CLIENT%\pak01_012.vpk"
mklink "%SERVER%\pak01_013.vpk" "%CLIENT%\pak01_013.vpk"
mklink "%SERVER%\pak01_014.vpk" "%CLIENT%\pak01_014.vpk"
mklink "%SERVER%\pak01_015.vpk" "%CLIENT%\pak01_015.vpk"
mklink "%SERVER%\pak01_016.vpk" "%CLIENT%\pak01_016.vpk"
mklink "%SERVER%\pak01_017.vpk" "%CLIENT%\pak01_017.vpk"
mklink "%SERVER%\pak01_018.vpk" "%CLIENT%\pak01_018.vpk"
mklink "%SERVER%\pak01_019.vpk" "%CLIENT%\pak01_019.vpk"
mklink "%SERVER%\pak01_020.vpk" "%CLIENT%\pak01_020.vpk"
mklink "%SERVER%\pak01_021.vpk" "%CLIENT%\pak01_021.vpk"
mklink "%SERVER%\pak01_022.vpk" "%CLIENT%\pak01_022.vpk"
mklink "%SERVER%\pak01_023.vpk" "%CLIENT%\pak01_023.vpk"
mklink "%SERVER%\pak01_024.vpk" "%CLIENT%\pak01_024.vpk"
mklink "%SERVER%\pak01_025.vpk" "%CLIENT%\pak01_025.vpk"
mklink "%SERVER%\pak01_026.vpk" "%CLIENT%\pak01_026.vpk"
mklink "%SERVER%\pak01_027.vpk" "%CLIENT%\pak01_027.vpk"
mklink "%SERVER%\pak01_028.vpk" "%CLIENT%\pak01_028.vpk"
mklink "%SERVER%\pak01_029.vpk" "%CLIENT%\pak01_029.vpk"
mklink "%SERVER%\pak01_030.vpk" "%CLIENT%\pak01_030.vpk"
mklink "%SERVER%\pak01_031.vpk" "%CLIENT%\pak01_031.vpk"
mklink "%SERVER%\pak01_032.vpk" "%CLIENT%\pak01_032.vpk"
mklink "%SERVER%\pak01_033.vpk" "%CLIENT%\pak01_033.vpk"
mklink "%SERVER%\pak01_034.vpk" "%CLIENT%\pak01_034.vpk"
mklink "%SERVER%\pak01_035.vpk" "%CLIENT%\pak01_035.vpk"
mklink "%SERVER%\pak01_036.vpk" "%CLIENT%\pak01_036.vpk"
mklink "%SERVER%\pak01_037.vpk" "%CLIENT%\pak01_037.vpk"
mklink "%SERVER%\pak01_038.vpk" "%CLIENT%\pak01_038.vpk"
mklink "%SERVER%\pak01_039.vpk" "%CLIENT%\pak01_039.vpk"
mklink "%SERVER%\pak01_040.vpk" "%CLIENT%\pak01_040.vpk"
mklink "%SERVER%\pak01_041.vpk" "%CLIENT%\pak01_041.vpk"
mklink "%SERVER%\pak01_042.vpk" "%CLIENT%\pak01_042.vpk"
mklink "%SERVER%\pak01_043.vpk" "%CLIENT%\pak01_043.vpk"
mklink "%SERVER%\pak01_044.vpk" "%CLIENT%\pak01_044.vpk"
mklink "%SERVER%\pak01_045.vpk" "%CLIENT%\pak01_045.vpk"
mklink "%SERVER%\pak01_046.vpk" "%CLIENT%\pak01_046.vpk"
mklink "%SERVER%\pak01_047.vpk" "%CLIENT%\pak01_047.vpk"
mklink "%SERVER%\pak01_048.vpk" "%CLIENT%\pak01_048.vpk"
mklink "%SERVER%\pak01_049.vpk" "%CLIENT%\pak01_049.vpk"
mklink "%SERVER%\pak01_050.vpk" "%CLIENT%\pak01_050.vpk"
mklink "%SERVER%\pak01_051.vpk" "%CLIENT%\pak01_051.vpk"
mklink "%SERVER%\pak01_052.vpk" "%CLIENT%\pak01_052.vpk"
mklink "%SERVER%\pak01_053.vpk" "%CLIENT%\pak01_053.vpk"
mklink "%SERVER%\pak01_054.vpk" "%CLIENT%\pak01_054.vpk"
mklink "%SERVER%\pak01_055.vpk" "%CLIENT%\pak01_055.vpk"
mklink "%SERVER%\pak01_056.vpk" "%CLIENT%\pak01_056.vpk"
mklink "%SERVER%\pak01_057.vpk" "%CLIENT%\pak01_057.vpk"
mklink "%SERVER%\pak01_058.vpk" "%CLIENT%\pak01_058.vpk"
mklink "%SERVER%\pak01_059.vpk" "%CLIENT%\pak01_059.vpk"
mklink "%SERVER%\pak01_060.vpk" "%CLIENT%\pak01_060.vpk"
mklink "%SERVER%\pak01_061.vpk" "%CLIENT%\pak01_061.vpk"
mklink "%SERVER%\pak01_062.vpk" "%CLIENT%\pak01_062.vpk"
mklink "%SERVER%\pak01_063.vpk" "%CLIENT%\pak01_063.vpk"
mklink "%SERVER%\pak01_064.vpk" "%CLIENT%\pak01_064.vpk"
mklink "%SERVER%\pak01_065.vpk" "%CLIENT%\pak01_065.vpk"
mklink "%SERVER%\pak01_066.vpk" "%CLIENT%\pak01_066.vpk"
mklink "%SERVER%\pak01_067.vpk" "%CLIENT%\pak01_067.vpk"
mklink "%SERVER%\pak01_068.vpk" "%CLIENT%\pak01_068.vpk"
mklink "%SERVER%\pak01_069.vpk" "%CLIENT%\pak01_069.vpk"
mklink "%SERVER%\pak01_070.vpk" "%CLIENT%\pak01_070.vpk"
mklink "%SERVER%\pak01_071.vpk" "%CLIENT%\pak01_071.vpk"
mklink "%SERVER%\pak01_072.vpk" "%CLIENT%\pak01_072.vpk"
mklink "%SERVER%\pak01_073.vpk" "%CLIENT%\pak01_073.vpk"
mklink "%SERVER%\pak01_074.vpk" "%CLIENT%\pak01_074.vpk"
mklink "%SERVER%\pak01_075.vpk" "%CLIENT%\pak01_075.vpk"
mklink "%SERVER%\pak01_076.vpk" "%CLIENT%\pak01_076.vpk"
mklink "%SERVER%\pak01_077.vpk" "%CLIENT%\pak01_077.vpk"
mklink "%SERVER%\pak01_078.vpk" "%CLIENT%\pak01_078.vpk"
mklink "%SERVER%\pak01_079.vpk" "%CLIENT%\pak01_079.vpk"
mklink "%SERVER%\pak01_080.vpk" "%CLIENT%\pak01_080.vpk"
mklink "%SERVER%\pak01_081.vpk" "%CLIENT%\pak01_081.vpk"
mklink "%SERVER%\pak01_082.vpk" "%CLIENT%\pak01_082.vpk"
mklink "%SERVER%\pak01_083.vpk" "%CLIENT%\pak01_083.vpk"
mklink "%SERVER%\pak01_084.vpk" "%CLIENT%\pak01_084.vpk"
mklink "%SERVER%\pak01_085.vpk" "%CLIENT%\pak01_085.vpk"
mklink "%SERVER%\pak01_086.vpk" "%CLIENT%\pak01_086.vpk"
mklink "%SERVER%\pak01_087.vpk" "%CLIENT%\pak01_087.vpk"
mklink "%SERVER%\pak01_088.vpk" "%CLIENT%\pak01_088.vpk"
mklink "%SERVER%\pak01_089.vpk" "%CLIENT%\pak01_089.vpk"
mklink "%SERVER%\pak01_090.vpk" "%CLIENT%\pak01_090.vpk"
mklink "%SERVER%\pak01_091.vpk" "%CLIENT%\pak01_091.vpk"
mklink "%SERVER%\pak01_092.vpk" "%CLIENT%\pak01_092.vpk"
mklink "%SERVER%\pak01_093.vpk" "%CLIENT%\pak01_093.vpk"
mklink "%SERVER%\pak01_094.vpk" "%CLIENT%\pak01_094.vpk"
mklink "%SERVER%\pak01_095.vpk" "%CLIENT%\pak01_095.vpk"
mklink "%SERVER%\pak01_096.vpk" "%CLIENT%\pak01_096.vpk"
mklink "%SERVER%\pak01_097.vpk" "%CLIENT%\pak01_097.vpk"
mklink "%SERVER%\pak01_098.vpk" "%CLIENT%\pak01_098.vpk"
mklink "%SERVER%\pak01_099.vpk" "%CLIENT%\pak01_099.vpk"
mklink "%SERVER%\pak01_100.vpk" "%CLIENT%\pak01_100.vpk"
mklink "%SERVER%\pak01_101.vpk" "%CLIENT%\pak01_101.vpk"
mklink "%SERVER%\pak01_102.vpk" "%CLIENT%\pak01_102.vpk"
mklink "%SERVER%\pak01_103.vpk" "%CLIENT%\pak01_103.vpk"
mklink "%SERVER%\pak01_104.vpk" "%CLIENT%\pak01_104.vpk"
mklink "%SERVER%\pak01_105.vpk" "%CLIENT%\pak01_105.vpk"
mklink "%SERVER%\pak01_106.vpk" "%CLIENT%\pak01_106.vpk"
mklink "%SERVER%\pak01_107.vpk" "%CLIENT%\pak01_107.vpk"
mklink "%SERVER%\pak01_108.vpk" "%CLIENT%\pak01_108.vpk"
mklink "%SERVER%\pak01_109.vpk" "%CLIENT%\pak01_109.vpk"
mklink "%SERVER%\pak01_110.vpk" "%CLIENT%\pak01_110.vpk"
mklink "%SERVER%\pak01_111.vpk" "%CLIENT%\pak01_111.vpk"
mklink "%SERVER%\pak01_112.vpk" "%CLIENT%\pak01_112.vpk"
mklink "%SERVER%\pak01_113.vpk" "%CLIENT%\pak01_113.vpk"
mklink "%SERVER%\pak01_114.vpk" "%CLIENT%\pak01_114.vpk"
mklink "%SERVER%\pak01_115.vpk" "%CLIENT%\pak01_115.vpk"
mklink "%SERVER%\pak01_116.vpk" "%CLIENT%\pak01_116.vpk"
mklink "%SERVER%\pak01_117.vpk" "%CLIENT%\pak01_117.vpk"
mklink "%SERVER%\pak01_118.vpk" "%CLIENT%\pak01_118.vpk"
mklink "%SERVER%\pak01_119.vpk" "%CLIENT%\pak01_119.vpk"
mklink "%SERVER%\pak01_120.vpk" "%CLIENT%\pak01_120.vpk"
mklink "%SERVER%\pak01_121.vpk" "%CLIENT%\pak01_121.vpk"
mklink "%SERVER%\pak01_122.vpk" "%CLIENT%\pak01_122.vpk"
mklink "%SERVER%\pak01_123.vpk" "%CLIENT%\pak01_123.vpk"
mklink "%SERVER%\pak01_124.vpk" "%CLIENT%\pak01_124.vpk"
mklink "%SERVER%\pak01_125.vpk" "%CLIENT%\pak01_125.vpk"
mklink "%SERVER%\pak01_126.vpk" "%CLIENT%\pak01_126.vpk"
mklink "%SERVER%\pak01_127.vpk" "%CLIENT%\pak01_127.vpk"
mklink "%SERVER%\pak01_128.vpk" "%CLIENT%\pak01_128.vpk"
mklink "%SERVER%\pak01_dir.vpk" "%CLIENT%\pak01_dir.vpk"
pause

Симлинки для L4D2:
PHP:
@echo off

REM Измените пути к папкам. Эти симлинки с серверной стороны будут указывать на клиентские файлы.
set "SERVER=C:\Servers\L4D2"
set "CLIENT=C:\Steam\SteamApps\common\left 4 dead 2"

REM ~2.61 GB
mklink /D "%SERVER%\left4dead2_dlc1" "%CLIENT%\left4dead2_dlc1"
mklink /D "%SERVER%\left4dead2_dlc2" "%CLIENT%\left4dead2_dlc2"
mklink /D "%SERVER%\left4dead2_dlc3" "%CLIENT%\left4dead2_dlc3"

REM ~10,200 files:
mklink /D "%SERVER%\left4dead2\scenes" "%CLIENT%\left4dead2\scenes"

REM ~2.20 GB
mklink "%SERVER%\left4dead2\pak01_000.vpk" "%CLIENT%\left4dead2\pak01_000.vpk"
mklink "%SERVER%\left4dead2\pak01_001.vpk" "%CLIENT%\left4dead2\pak01_001.vpk"
mklink "%SERVER%\left4dead2\pak01_002.vpk" "%CLIENT%\left4dead2\pak01_002.vpk"
mklink "%SERVER%\left4dead2\pak01_003.vpk" "%CLIENT%\left4dead2\pak01_003.vpk"
mklink "%SERVER%\left4dead2\pak01_004.vpk" "%CLIENT%\left4dead2\pak01_004.vpk"
mklink "%SERVER%\left4dead2\pak01_005.vpk" "%CLIENT%\left4dead2\pak01_005.vpk"
mklink "%SERVER%\left4dead2\pak01_006.vpk" "%CLIENT%\left4dead2\pak01_006.vpk"
mklink "%SERVER%\left4dead2\pak01_007.vpk" "%CLIENT%\left4dead2\pak01_007.vpk"
mklink "%SERVER%\left4dead2\pak01_008.vpk" "%CLIENT%\left4dead2\pak01_008.vpk"
mklink "%SERVER%\left4dead2\pak01_009.vpk" "%CLIENT%\left4dead2\pak01_009.vpk"
mklink "%SERVER%\left4dead2\pak01_010.vpk" "%CLIENT%\left4dead2\pak01_010.vpk"
mklink "%SERVER%\left4dead2\pak01_011.vpk" "%CLIENT%\left4dead2\pak01_011.vpk"
mklink "%SERVER%\left4dead2\pak01_012.vpk" "%CLIENT%\left4dead2\pak01_012.vpk"
mklink "%SERVER%\left4dead2\pak01_013.vpk" "%CLIENT%\left4dead2\pak01_013.vpk"
mklink "%SERVER%\left4dead2\pak01_014.vpk" "%CLIENT%\left4dead2\pak01_014.vpk"
mklink "%SERVER%\left4dead2\pak01_dir.vpk" "%CLIENT%\left4dead2\pak01_dir.vpk"
pause

Установка MetaMod и SourceMod:

  • Чтобы плагины могли запускаться, вам необходимо установить MetaMod и SourceMod.
  • Рекомендуется держать их обновлёнными до последней стабильной версии, чтобы получать все патчи безопасности, увеличение производительности и новый функционал.
  • Managing your Sourcemod installation - подробности о структуре папок SM, установке плагинов и расширений.

Другие ссылки:


3. Подключение к серверу:
Инфа от Dragokas:​

Чтобы разрешить другим людям подключаться к вашему серверу через Интернет:​
  • Настройте переадресацию портов для порта 27016 (или другого, используемого вашей игрой).
  • Настройте сетевой экран на разрешение этого же порта (tcp/udp). Больше подробностей: здесь и здесь.
  • (только для CS:GO) Создайте токен и укажите его в файле csgo\cfg\autoexec.cfg, прописав: sv_setsteamaccount <login_token>
1. Другие люди должны ввести в консоль:​
Код:
connect Ваш.публичный.IP:27016

2. Если ВЫ желаете подключиться к своему выделенному серверу:​
- Введите в клиентскую консоль:​
Код:
connect Локальный.IP:27016

Важное замечание для п.2:
  • Ваш клиент игры должен быть запущен ДО момента запуска сервера!
    Иначе, Steam откажет вам в запуске клиента. Таким образом, позднее вы сможете запустить клиент игры только вручную через .exe файл игры - такое сработает не со всеми играми, и только с параметром -insecure.​
  • Обычно я использую такой бат-файл, чтобы автоматически запустить клиент, выждать 3 сек, и затем запустить сервер:
PHP:
:: Для CS:GO
SET "appID=730"
SET "GAME_ALIAS=csgo"
SET "MAP_NAME=de_dust2"

:: укажите путь до файла srcds.exe вашего выделенного сервера
SET "SERVER_DIR=E:\server\csgo"

:: Пример для L4D2 - удалите префиксы "::"
::SET "appID=550"
::SET "GAME_ALIAS=left4dead2"
::SET "MAP_NAME=c1m1_hotel"
::SET "SERVER_DIR=V:\server\l4d2"

SET "IP=192.168.31.4"
SET "PORT=27016"

:: Удалите ненужные опциональные аргументы по желанию
SET OPTIONAL_ARG= -insecure +sv_pure 0 +sv_consistency 0 -debug -condebug

:: укажите путь к файлу steam.exe
SET "STEAM_DIR=C:\Program Files (x86)\Steam"

:: ===============================

SET "SERVER_ARG= -console -game %GAME_ALIAS% +hostport %PORT% +clientport 27006 +map %MAP_NAME% %OPTIONAL_ARG%"
SET "STEAM_ARG= -applaunch %appID% -dev -console -sw +sv_lan 1 -novid +connect %IP%:%PORT%"

:: ===============================

:: Мы запускаем клиент прежде, чем сервер
start "" "%STEAM_DIR%\steam.exe" %STEAM_ARG%

timeout /t 3

:: Запускаем сервер
START "" /min "%SERVER_DIR%\srcds.exe" %SERVER_ARG%

Пожалуйста, напишите в комментариях, если вы знаете способ по-лучше, как подключиться локально к своему серверу, если он уже был запущен (перед клиентом).
 
Последнее редактирование:
4. Окружение для написания сценариев:
- У каждого свои вкусы. Вот несколько примеров редакторов:​
- Пожалуйста, предлагайте ещё ссылки.​
Редакторы под SourcePawn:
  • BasicPawn - "легковесный и простой редактор SourcePawn, который использует динамическое авто-дополнение кода и IntelliSense"
  • SPEdit - "это редактор Sourcepawn с несколькими фишками, облегчающими процесс написания кода sp"
Текстовые редакторы:
- NotePad++ (поддерживает различные языки программирования.)​

Cообщество SourceMod сделало различные модификации под SourcePawn:​
  • Настройка Notepad++ для SourceMod.
  • Лично я использую это, синтаксическую подсветку, функции авто-завершения, горячие клавиши для компиляции и копирования плагинов на сервер, а также отображение предупреждений и ошибок с возможностью двойного клика для перехода к проблемной строке.
  • В N++ я использую следующее:
Плагины:
  • Compare
  • DSpellCheck
  • TextFX Characters
  • NppExec
Скрипт NppExec:
  • Я его использую для компиляции и копирования плагинов в различные игры в зависимости от того, для какой провожу тесты. Убрав знаки // перед "set GAME" он скомпилирует и скопирует в соответствующую игру, например, вот этот вариант будет работать с папкой игры CS:GO. Не имеет значения, где именно сохранён скрипт .sp.
PHP:
set GAME = source2007\gesource
set GAME = CSGO\csgo
//set GAME = TF2\tf
//set GAME = L4D\left4dead
//set GAME = L4D2\left4dead2

set COMPILER = C:\Servers\$(GAME)\addons\sourcemod\scripting\spcomp.exe
set COMPILE_FOLDER = C:\Servers\$(GAME)\addons\sourcemod\plugins

NPP_SAVE
cd "$(CURRENT_DIRECTORY)"
$(COMPILER) "$(CURRENT_DIRECTORY)\$(NAME_PART).sp"
cmd /q /c copy "$(CURRENT_DIRECTORY)\$(NAME_PART).smx" "$(COMPILE_FOLDER)\$(NAME_PART).smx"
cmd /q /c del /q "$(CURRENT_DIRECTORY)\$(NAME_PART).smx"

- Sublime Text (поддерживает различные языки программирования)​

Сообщество SourceMod сделало различные модификации под SourcePawn:​
 
Последнее редактирование:
5. Полезные плагины и расширения для разработчиков
  • Stripper:Source Extension - Мощный инструмент для модификации карты и создания дампа.
  • Accelerator Extension - Сбор отчётов о краше для отладки при падении сервера.
  • Plugin Reload - (обновлённая версия) Автоматически перезагружает плагин, когда изменяется его штамп времени
  • Plugin - AutoReload - (старая версия) Автоматически перезагружает плагин, когда изменяется его штамп времени
  • Cvar List and Commands List - Выводит в лог список всех кваров и команд
  • tEntDev - Позволяет вам следить за изменением сетевых параметров (netprops) сущности (Последний)
  • Event Info - Логирование хуков всех событий динамически через .res файлы.
  • Dev Cmds - Предоставляет кучу команд для администраторов и разработчиков.
  • Gamedata Offset Tester - Печать байтов памяти в серверную консоль для поиска смещений.
  • Gamedata Sig Tester - Проверка байтов сигнатуры gamedata.
  • Input Hooks - Печать перехватываемых при поступлении на вход сущности команд ввода, с фильтрацией по имени класса.
  • TempEnt Hooks - Печать данных временных сущностей (TempEnt), с фильтрацией по имени класса.
  • UserMsg Hooks - Печать данных пользовательских сообщений (UserMessage), с фильтрацией по имени класса.
  • Entity Limits Logger - выводит в лог подробную информацию о всех сущностях, если их общее кол-во превысило порог.
  • Cvar Configs Updater - Хорош для обновления конфигов кваров, и удаления более неиспользуемых (Прим. переводчика: незаменим при обновлении плагинов, и полезен для отключения лишних кваров (с дефолтовыми значениями) с целью уменьшения риска бага с переполнениям буфера чтения).
  • Command and ConVar - Buffer Overflow Fixer - Нужен, когда сервер имеет слишком много кваров и не в состоянии прочитать их все одновременно.
  • ConVars Anomaly Fixer - Хорош для проверки и тестирования кваров и конфигов на ошибки (Прим. переводчика: альтернатива плагину выше).
  • String Tables Dumper - Сохраняет записи всех строковых таблиц игры.
 
Последнее редактирование:
6. Серверные фиксы - (плагины + расширения под различные игры):

Любые игры:
CS:S:
  • Auto .nav Files Generator by shavit
    - Creates (just copies base.nav file actually) .nav files for new maps without .nav. Some bhop replay bots will not spawn if there's no .nav generated.

  • Momentum Mod Surf Fix by butare
    - Surf/ramp fix for ramps that are glitchy and stop you when you surf on them (base on momentum mod fix).

  • MultiPlayer Bunny Hops: Source by DaFox
    - Allows you to jump on bhop blocks that are func_doors multiple times (that's a thing for old bhop maps), useful when there's multiple players jumping on same block and failing.

  • RNGFix by rio_
    - RNGFix that removes some RNG based stuff (has some features for bhop and surf).
CS:GO:
  • Auto .nav Files Generator от shavit
    - Создаёт (на самом деле просто копирует base.nav файл) .nav файлы для новых карт, у которых отсутствует .nav. Некоторые bhop реплей боты не спавнятся, когда нет сгенерированного файла .nav.

  • Momentum Mod Surf Fix от butare
    - Исправление для сёрфинга/рамп, которые глючат и останавливаются, когда вы катаетесь по ним (основано на исправлении из momentum mod)

  • Movement Unlocker от Peace-Maker
    - Удаляет ограничение максимальной скорости игроков на земле. Чувствуется как CS:S.

  • MultiPlayer Bunny Hops: Source от DaFox
    - Позволяет вам множество раз прыгать на bhop блоки func_doors (эта штука для старых bhop карт); полезно, когда несколько игроков прыгают на один и тот же блок, промахиваясь.

  • Particle Auto-Precacher от gunslinger23
    - Позволяет кешировать партикли на картах.

  • Particle System Fix - Cleanup Particles on Map End от SHUFEN.jp
    - Функция удаления кешированных партиклей в CS:GO сломана; этот фикс снимает кеширование и возвращает чистое состояние строковым таблицам.

  • RNGFix от rio_
    - RNGFix удаляет некоторые вещи, основанные на RNG (имеет некоторые функции для bhop и сёрфинга).

  • Server Lagger Exploit Security Patch - от backwards
    - Предотвращает возможность пользоваться эксплойтом, создающем лаги и рывки на сервере.

  • Trigger_push Fix от blaacky, обновлён hmmmmm
    - Фикс для trigger_push; делает его более плавным.
L4D1:
L4D2:
 
Последнее редактирование:
7. Различные функции и информация:

Hammer и Authoring Tools:
  • Для L4D/2 перейдите в клиент Steam > Библиотеки > Снимите галочку "Показать только готовые к запуску игры" (в старых версиях - это раздел "Инструменты"), найдите в списке и скачайте L4D/2 Authoring Tools. Он включает в себя Model Viewer (просмотрщик моделей) - полезен для определения индексов анимации и точек крепления, используемых для фишек вроде "SetParent". Вы можете распаковать модель .mdl в папку вашей игры /models/ чтобы отрыть её в этом инструменте.
  • CS:GO hammer доступен только после покупки обновления с Prime статусом на странице магазина Steam. Ссылки: здесь и здесь.

  • Расширение Stripper:Source - Вместо декомпиляции карты, с помощью этого инструмента можно сделать дамп данных карты и уже в нём смотреть, что и как работает.
    - Я использовал это и hammer для воспроизведения через плагин различных функций, реализованных на картах для L4D2.
    - Stripper также полезен для изменения сущностей на картах. Почитайте его тему для больших подробностей.

Партикли (так называемые частицы, Particles):
  • В хаммере (по крайней мере в L4D2) вы можете увидеть частицы, указав их в сущности "info_particle_system", сделав двойной клик на "Particle System Name" в списке keyvalues. Это откроет "Браузер частиц". Редактор частиц доступен через "L4D Authoring Tools" -> L4D (Tools Mode).
  • Также есть [Batch] Пакетный распаковщик частиц & плагин тестирования и руководство от Dragokas.
  • Некоторые или большинство из игр на движке source также включают внутри-игровой Редактор частиц (см. по ссылке, как его включить), полезен для отображения и создания сторонних частиц. Вы можете воспользоваться функцией SourceMod-а AddFileToDownloadsTable для передачи клиентам сторонних моделей, звуков, частиц и т.д.
  • Чтобы пре-кешировать частицы используйте следующий stock и вызывайте его в OnMapStart();
PHP:
public void OnMapStart()
{
    PrecacheParticle("flare_burning");
}

int PrecacheParticle(const char[] sEffectName)
{
    static int table = INVALID_STRING_TABLE;

    if( table == INVALID_STRING_TABLE )
    {
        table = FindStringTable("ParticleEffectNames");
    }

    int index = FindStringIndex(table, sEffectName);
    if( index == INVALID_STRING_INDEX )
    {
        bool save = LockStringTables(false);
        AddToStringTable(table, sEffectName);
        LockStringTables(save);
        index = FindStringIndex(table, sEffectName);
    }

    return index;
}

AddFileToDownloadsTable: (сторонний контент)​

Свойства сущности: (Prop_Data и Prop_Send)​
  • NetProps и DataMaps являются игровыми переменными, используемыми сущностями для хранения данных о себе. NetProps передаются клиентам, тогда как DataMaps доступны только для сервера.
  • Их можно читать с помощью: GetEntProp, GetEntPropArraySize, GetEntPropEnt (для получения сущности или индекса клиента из свойства сущности), GetEntPropFloat, GetEntPropFloat, GetEntPropVector и изменять через соответствующие SetEntProp* функции. HasEntProp позволит вам проверить, существует ли у сущности указанное вами имя свойства.
  • SourceMod предоставляет несколько команд для сбора полезной информации. Чтобы просмотреть полный список, введите find sm_dump в консоль сервера.
  • Введите следующие команды в вашу серверную консоль для генерации и сохранения отчётов в корневую папку игры (рядом с addons, bin, cfg, maps):
  • Команды для дампа всех свойств netprops и datamaps у всех сущностей:
    - Формат лога "datamaps": "имя свойства" ("смещение") ("тип") ("размер") - "KeyValue" (применяется в функциях DispatchKeyValue*).
    sm_dump_netprops_xml netprops.xml
    sm_dump_netprops netprops.txt
    sm_dump_datamaps datamaps.txt
  • Имена классов сущностей выводятся в формате: серверное имя класса (GetEntityNetClass) - имя класса сущности (GetEdictClassname).
    sm_dump_classes classes.txt
  • Список временных сущностей (TempEnts) и их формат.
    sm_dump_teprops teprops.txt

События:
  • Игра возбуждает события, которые затем плагины могут перехватить (хукнуть) для выполнения различных задач, когда наступают специфические условия, вызывающие эти события.
  • Events (SourceMod Scripting) - Подробности о том, как искать, хукать, создавать, блокировать и изменять события.
  • Game Events (Source) - Список разнообразных событий, общих и специфических для конкретной игры событий (Некоторые данные могут отсутствовать или быть устаревшими).
  • Вы можете воспользоваться GCFScape, чтобы открыть ваши игровые файлы .VPK и извлечь оттуда различные .res файлы для поиска событий ("events") в папке resource.

Звуковые хуки:
  • Звуковые хуки могут изменять громкость, воспроизводимый файл или блокировать звуки.
PHP:
#include <sourcemod>
#include <sdktools>
#include <sdkhooks>

bool g_bBlockSound;

public void OnPluginStart()
{
    // ПРЕДУПРЕЖДЕНИЕ:  Избегайте хука более, чем один раз, хукайте только единожды.
    // Вы можете, к примеру, использовать статический или глобальный bool, допустим g_bHooked, чтобы избежать множественный хуков звука.
    // Вы можете хукать, где угодно. Хук не обязательно должен быть в OnPluginStart.

    // Большинство звуков воспроизводятся через это:
    AddNormalSoundHook(SoundHook);

    // Однако, некоторые звуки могут воспроизводиться только здесь:
    AddAmbientSoundHook(AmbientHook));
}

public void OnPluginEnd()
{
    // Вам не нужно снимать хук в OnPluginEnd, просто показываю, что вы можете это сделать, если вам больше не нужен хук.
    RemoveNormalSoundHook(SoundHook);
    RemoveAmbientSoundHook(AmbientHook);
}

// Слежение за броском гранат.
public void OnEntityCreated(int entity, const char[] classname)
{
    if( strncmp(classname, "molotov_projectile", 13) == 0 )
    {
        SDKHook(entity, SDKHook_SpawnPost, SpawnPost);
    }
}

public void SpawnPost(int entity)
{
    RequestFrame(OnNextFrame);
    g_bBlockSound = true;
}

public void OnNextFrame()
{
    g_bBlockSound = false;
}

public Action SoundHook(int clients[MAXPLAYERS], int &numClients, char sample[PLATFORM_MAX_PATH], int &entity, int &channel, float &volume, int &level, int &pitch, int &flags, char soundEntry[PLATFORM_MAX_PATH], int &seed)
{
    // Поскольку SoundHook вызывается для каждого воспроизводимого звука, лучше всего избегать сравнения строки, на сколько это возможно
    if( g_bBlockSound )
    {
        // Блокируем звук, когда бросается молотов
        if( strcmp(sample, "weapons/molotov/fire_loop_1.wav") == 0 )
        {
            return Plugin_Handled;
        }
    }

    // Уменьшаем громкость взрыва
    if( strcmp(sample, "weapons/molotov/molotov_detonate_3.wav") == 0 )
    {
        volume = 0.5;
        return Plugin_Changed;
    }

    // Без изменений
    return Plugin_Continue;
}

public Action AmbientHook(char sample[PLATFORM_MAX_PATH], int &entity, float &volume, int &level, int &pitch, float pos[3], int &flags, float &delay)
{
    // Тоже самое, что и выше, только для звуков окружения (Ambient Sounds).
}

SetTransmit: (Прячет модели от отдельных игроков):​
  • SDKHook_SetTransmit может быть использован, чтобы спрятать от клиентов модели, частицы и некоторые другие типы сущностей (например, light_dynamic).
  • Попытка спрятать частицы может не сработать в некоторых играх или например, может сработать только на Windows. Это ограничения движка.
  • Попытка спрятать некоторые сущности может не сработать, например, prop_glowing_object в L4D1.
  • Инфа от asherkin: CBaseEntity::SetTransmit является очень горячей функцией, поэтому у движка есть множество оптимизаций, чтобы предотвращать излишние её вызовы. Обычно, по этому пути идут сущности с флагами FL_EDICT_FULLCHECK, FL_EDICT_ALWAYS и FL_EDICT_PVSCHECK. Для полного понимания, можно обратиться к ключевой логике в CServerGameEnts::CheckTransmit из SDK.
  • Два примера, для пропов и частиц:

Модели:
PHP:
bool g_bShowProp[MAXPLAYERS+1];
public Action CmdProp(int client, int args)
{
    // Предположим, что мы создали сущность "prop", но нам хочется показать её только конкретным клиентам
    int entity = CreateEntityByName("prop_dynamic");
    // Остальные вещи для сущности. SetEntityModel() и т.д. Опускаем здесь, для краткости примера.

    // Теперь мы хукаем её. Функция обратного вызова (колбэк) Hook_SetTransmit вызывается с каждым фреймом, поэтому здесь внутри лучше использовать простые расчёты
    SDKHook(entity, SDKHook_SetTransmit, Hook_SetTransmit);

    // Существуют разные способы, чтобы это сделать. Советую посмотреть другие плагины в качестве примеров.
    // Мы собираемся сохранить здесь значение, чтобы определять, можно ли клиенту видеть сущность или нет.
    g_bShowProp[client] = true;
}

public Action Hook_SetTransmit(int entity, int client)
{
    // Проверяем, разрешено ли клиенту видеть сущность
    if( g_bShowProp[client] == true )
        return Plugin_Continue; // Это разрешит сущности быть видимой для клиента.
    return Plugin_Handled; // Это спрячет её от клиента, если клиенту не разрешается видеть её.
}

Частицы:
PHP:
bool g_bShowProp[MAXPLAYERS+1];
public Action CmdProp(int client, int args)
{
    // Предположим, мы создали частицу, но желаем отобразить её только для конкретных игроков
    int entity = CreateEntityByName("info_particle_system");
    // Остальные свойства сущности. DispatchSpawn() и т.д. Опускаем, чтобы сократить пример.

    SDKHook(entity, SDKHook_SetTransmit, Hook_SetTransmit);

    g_bShowProp[client] = true;
}

public Action Hook_SetTransmit(int entity, int client)
{
    // Проверка, позволено ли клиенту видеть проп
    if( g_bShowProp[client] == true )
        return Plugin_Continue; // Это разрешит пропу быть видимым для клиента

    // Следующие две строки используются, чтобы прятать частицу
    // Это, вероятно, может заработать только в конкретных играх, либо только на серверах Windows
    if( GetEdictFlags(entity) & FL_EDICT_ALWAYS )
        SetEdictFlags(entity, GetEdictFlags(entity) &~ FL_EDICT_ALWAYS);

    return Plugin_Handled;
}

RequestFrame: (выполнение кода с задержкой времени на 1 фрейм)​
  • CreateTimer() имеет минимальное значение в 0.1 секунду. Для всего, что быстрее (например, следующий фрейм игры) лучше использовать RequestFrame().
  • Когда вы определяете снаряды (projectiles) в OnEntityCreated() и используете SDKHook_SpawnPost(), данные о скорости здесь всё ещё не инициализированы. Вот где я использую RequestFrame() для получения действительных данных.
PHP:
// Слежение за броском гранат
public void OnEntityCreated(int entity, const char[] classname)
{
    if( strcmp(classname, "molotov_projectile") == 0 )
    {
        // Ждем, пока сущность полностью заспавнится (появится)
        SDKHook(entity, SDKHook_SpawnPost, SpawnPost);
    }
}

public void SpawnPost(int entity)
{
    // 1 фрейм требуется для получения скорости
    RequestFrame(OnNextFrame, EntIndexToEntRef(entity));
}

public void OnNextFrame(int entity)
{
    // Верифицируем сущность
    if( EntRefToEntIndex(entity) == INVALID_ENT_REFERENCE || !IsValidEntity(entity) )
        return;

    // Теперь скорость действительна и не равна 0,0,0
    float vPos[3];
    GetEntPropVector(entity, Prop_Send, "m_vInitialVelocity", vPos);
}

  • Переключение игроков в команду Зрителей сразу же после подключения. Это предотвращает от каких-либо задержек, из-за которых их могло бы успеть заспавнить сперва где-либо ещё (спасибо "Maxximou5" и "Desktop" за пример):
PHP:
#include <cstrike>

public void OnPluginStart()
{
    HookEvent("player_connect_full", Event_PlayerConnectFull);
}

public void Event_PlayerConnectFull(Event event, const char[] name, bool dontBroadcast)
{
    int client = GetClientOfUserId(event.GetInt("userid"));
    RequestFrame(Frame_ChangeTeam, client);
}

public void Frame_ChangeTeam(any client)
{
    ChangeClientTeam(client, CS_TEAM_SPECTATOR);
}

Разное:
  • Цвет текста (инклуды): Colors.inc, stamm-colors.inc, morecolors.inc, multicolors.inc, colorvariables.inc, colorsLib.inc
  • SteamID типы: Steam2, Steam3 & Steam64. Используйте GetClientAuthId для получения нужного вам типа. Хороши для хранения в файл/базе данных для восстановления после перезагрузки.
  • KeyValue конфиги - Они необходимы для многих вещей. Настройки данных, чтение, сохранение. Например, KeyValues SourceMod Scripting.
  • SMCParser - Подобны KeyValue конфигам, но не требуют заранее знать имя ключа. Примеры см. в папке со скриптами SM scripting/admin-flatfile. Или в моих VScript Replacer, Info Editor или Neon Beams, как наиболее простая версия. Прим. переводчика: является более надёжным инструментом, совместим с форматом KeyValues файлов, но в отличии от последнего поддерживает многострочные комментарии).
  • Client Preferences (предпочтения клиента) - это куки, которые используются для хранения клиентских данных. [Tutorial] ClientPrefs. Хранение простых настроек для каждого из клиентов. Прим. переводчика: по своей сути представляют из себя записи в базе данных, хранящейся на сервере (файл data/sqlite/clientprefs-sqlite.sq3).
  • GCFScape - Для открытия VPK и получения файлов, к примеру "modevents.res" в качестве источника для событий, используемых игрой (другие источники по большей части устарели). Также для открытия .BSP и извлечения ресурсов карты.
  • VProf - Для проверки производительности плагинов. Используйте только в тестовой среде, а не на живом сервере, т.к. эта проверка может быть слишком интенсивной.
  • SM profiler API - Для запуска проверок скорости работы (бенчмаркинга) определённых участков вашего плагина.
  • Timers - Можно использовать для задержки задач, например, сообщений с приветствием, или для повторения чего-либо несколько раз в течении определённого интервала. См. сообщение со множеством примеров правильного использования.
  • Translations - Используется для многоязычной поддержки, когда на экран отображаются сообщения или перевод в менюшках и пр.


Информация:
  • edict могут иметь индексы только в пределах от 0 до 2048. Больше 2048 имеют не передающиеся по сети сущности (non-networked entities), которые может увидеть только сервер (например, сущность logic_case).
    - Новые игры имеют более высокие лимиты.
    - CS:GO может использовать до 4096 под edicts (Я предполагаю, что 4097 - 8192 - выделены для не передающихся по сети сущностей).
    - Garrys Mod очевидно поддерживает до 8192 под edict-ы.
  • IsValidEdict() возвращает false для non-networked сущностей. Для таких, используйте IsValidEntity().
  • FindEntityByClassname() для non-networked сущностей возвращает ссылку на сущность (к примеру, номер сущности будет чем-то вроде: -2036529131).
    - Вы можете преобразовать её обратно в индекс сущности с помощью EntRefToEntIndex() и получить значение между 2049-4096. Однако, если вы не планируете использовать его в качестве индекса для массива, то лучше ограничиться использованием ссылки.
  • Format - Используйте при форматировании строки, когда буфер назначения является также одним из параметров.
    PHP:
    Format(buffer, sizeof buffer, "%i %s", someInt, buffer);
  • FormatEx - Используйте, когда форматируемый и целевой буферы НЕ совпадают (она быстрее чем Format).
    PHP:
    FormatEx(buffer, sizeof buffer, "%i", someInt);
  • strcopy - Используйте для копирования строки, не использующей правила форматирования.
    PHP:
    strcopy(buffer, sizeof buffer, "Some string");
  • StrCat - Используйте для добавления в конец целевой строки, без необходимости в форматировании.
    PHP:
    StrCat(buffer, sizeof buffer, "this text is appended to buffer");
  • report_entities - считает кол-во одинаковых классов энтити на карте и выводит отчёт в серверную консоль
  • cl_showents клиентский квар - выводит список сущностей и их индексы (требует sv_cheats 1).
  • cl_showpos 1 клиентский квар - для открытия мини-панели, отслеживающей вашу текущую позицию, угол, скорость.
  • net_graph клиентский квар - позволяет вам увидеть графики fps, ping, lerp, нагрузку на сеть и т.п. Больше инфы.
    - net_graph 4 - рекомендован.
    - Вот примеры биндов, которые я использую. Когда нажимаешь на Tab для открытия табло с игроками, то одновременно отображается и net_graph:
    PHP:
    bind tab +tabscore;
    alias +tabscore "+showscores; net_graph 4";
    alias -tabscore "-showscores; net_graph 0";
    net_graphpos 0 // default 1; Left = 0; Right = 1; Centre = 2
  • Клиентские квары для симуляции лагов (можно использовать для тестов). Требуется включённый sv_cheats:
    - net_fakejitter - фейковые колебания пинга (Jitter)
    - net_fakelag - лаг для всех входящий сетевых данных (включая петлю) на указанное кол-во миллисекунд.
    - net_fakeloss - Симулировать потерю пакетов в процентах (негативное число означает потерю 1/n пакетов)
    - net_droppackets - Потерять следующие N пакетов у клиента
    - net_chokeloop - Применить колебание на полосе пропускания для петлевых пакетов
  • sv_allow_wait_command - Серверный квар, разрешающий или запрещающий команду wait для клиентов, подключённых к серверу.
    - Это приведет к вылету клиента, если он однажды использовал команду wait, а сервер отключил её без перезапуска игры клиентом.
 
Последнее редактирование:
8. Продвинутые техники:
Объектно-ориентированное API

Методмапы (MethodMaps)
Структуры (Enum Structs)
Нативы и форварды (Natives and Forwards)
  • Создание нативов - Вкратце, это функция, которую создаёт ваш плагин, чтобы другие плагины имени возможность нею воспользоваться.
  • Создание форвардов - Вкратце, это событие, которое создаёт ваш плагин, чтобы оповещать другие плагины, когда что-то происходит.
Препроцессорные директивы компилятора

Лучи трассировки
  • ToDo (планируется).

Коллизии

Навигация

VTables - Используются для вызовов SDKCall, Detour (перехвата функций) и патчей памяти.​
GameData сигнатуры - Используются для SDKCall вызовов, перехватов внутренних функций игровых библиотек (Detours) и патчей памяти.​
Патч (модификация) памяти
SDKCall вызовы
  • Вызов функций - Вы можете вызывать внутри-игровые функции, чтобы создавать уникальные вещи.
Хуки SDKHook (Расширение теперь является частью SourceMod):​
  • Расширение SDKHooks - Множество полезных функций и форвардов, например хук OnTakeDamage (получение урона), PreThink сущности (вызывается в момент "обдумывания" физических и др. процессов сущности) и форварды OnEntityCreated/Destroyed (вызываются перед созданием/уничтожением сущности) и т.д.
  • Обновлённый список и прототипы колбеков всегда доступны в инклуд файле.
Перехват или "обход" функций (Detouring)
  • Расширение DHooks - Создаёт сторонние хуки игровых функций для их блокировки, чтения или изменения. Также полезен для создания новых событий и форвардов.
 
Последнее редактирование:
9. Лучшие практики:

  • Используйте RemoveEntity(entity); (в SM 1.10+), которая является быстрой и безопасной версией AcceptEntityInput(entity, "Kill"); (в SM <= 1.9) по сравнению с RemoveEdict(entity).
    - RemoveEdict в некоторых случаях может быть безопасной и хорошей для создания + удаления сущности в одной и той же функции, когда эта сущность вам едва ли потребуется в дальнейшем, например point_hurt и env_shake.

  • Перед использованием или убийством сущности убедитесь, что она действительна с помощью IsValidEntity();
    - Обязательно проверяйте на ноль if(entity != 0) или просто if(entity), потому что 0 (на выделенном сервере) - это сущность worldspawn и является действительной, таким образом вы можете мгновенно обвалить сервер.

  • Не имеет смысла в использовании if IsClientConnected(client) && IsClientInGame(client) одновременно, поскольку IsClientInGame уже включает проверку IsClientConnected.

  • Используйте единый стиль именования переменных. Хорошее начало - "Венгерская нотация"
    - например, предваряя имя переменной префиксом g_ в случае, когда она объявляется как глобальная.

  • Используйте TIMER_FLAG_NO_MAPCHANGE для закрытия хендлов таймера автоматически по завершению карты. Детальнее с примерами см. дополнение "Как остановить (удалить) глобальный таймер".

  • !!iValue - для преобразования "int" в "bool"

  • Лично я объявляю переменные типа "char" как "static char", если они большие и вызываются несколько раз в дорогостоящих циклах или в Think* функциях.
    - Это оптимизирует скорость, потому что для такой переменной не требуется повторное выделение памяти. НО: Эта память никогда не уничтожается и навсегда остаётся там.
    - Используйте это с умом, т.е. только, когда вы действительно понимаете, что делаете. Это на самом деле даёт выигрыш для дорогостоящих в плане производительности функций и серверах с высоким тиком и 50-100+ сумасшедшими плагинами, где оптимизация становится просто необходима.
    - Однако: "использование static для локальных массивов - не является хорошим советом, это приводит к множеству багов, в тоже время выделение памяти в стеке - дешёвая операция." - asherkin.

  • Если вы желаете присвоить одно и тоже значение всем элементам массива, вы можете это сделать прямо во время инициализации, а не через цикл позднее: int test[32] = {1, ...};
    - В противном случае int test[32] заполняется значениями по умолчанию, в данном случае 0.

  • TeleportEntity должен идти перед DispatchSpawn. Если сделать в обратном порядке, это может привести к крашу сервера. Такое случается с некоторыми типами моделей/сущностей, которые спавнятся в координатах 0,0,0.

  • switch вместо множества условий if else. Это вероятно ещё и быстрее. Больше информации здесь.
    - case секция использует константные значения. Она также поддерживает множественные значения, например case 1,2:

  • c[0] == 'a' вместо полного сравнения строки, к примеру, когда вы проверяете список известных моделей игроков. Найдите индекс в строке, где для каждой из них знак будет уникальным. Можно использовать совместно со switch().
    - Хороший пример от Lux можно найти здесь.

  • strcmp вместо StrEqual. Последнее - просто обёртка от первого и будет работать медленнее из-за создания дополнительного вызова.

  • FormatEx вместо Format. Используйте только, когда входящий и выходной буферы НЕ одинаковые.

  • FormatEx(blah, sizeof blah, "%d", i); - слегка быстрее, чем IntToString(i, blah, sizeof blah); но в целом, сравнения не делал, так что используйте любую.
    - Однако, обе они в 3 - 10x раз быстрее, чем Format(blah, sizeof blah, "%d", i);

  • StringMap.GetValue слегка быстрее, чем ArrayList.FindString и вероятно значительно быстрее на огромных массивах.

  • Предпочитайте SDKHook_Think и похожие функции вместо использования OnGameFrame.

  • Создавайте один таймер для всех игроков вместо отдельного таймера для каждого из них (например: плагины для регенерации здоровья).

  • delete и null являются предпочтительней CloseHandle и INVALID_HANDLE. delete также устанавливает хендл равным null, таким образом вам не нужно заботится об этом.

  • Слежение за кварами, передавая их хендл в HookConVarChange и хранение возвращаемого значения в глобальные переменные - является более оптимальным по скорости, чем постоянный вызов GetConvar*.
    - Посмотрите на любой из моих плагинов для примера.

  • Используйте %N вместо GetClientName() где это возможно, например, при использовании Format.

  • Поздняя загрузка - Больше инфы здесь.
    - Поздняя загрузка (Late loading) - это когда плагин был вероятно загружен вручную во время игры, или перезагружен.
    - Из-за этого, плагин может начать работать не так, как было предусмотрено, потому что сработают не все события.
    - Вы можете определить, что плагин загружен как поздний и соответственно настроить всё, как положено при нормальной загрузке.
    PHP:
    bool g_bLateLoad;
    public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
    {
        g_bLateLoad = late;
        return APLRes_Success;
    }
    
    public void OnPluginStart()
    {
        if( g_bLateLoad )
        {
            for( int i = 1; i <= MaxClients; i++; )
            {
                if( IsClientInGame(i) )
                {
                    // Делаем что-либо специфическое для клиентов, которые уже находятся в игре, если они подключились до того, как плагин был загружен.
                    OnClientPutInServer(i);
                }
            }
        }
    }
    
    public void OnClientPutInServer(int client)
    {
        // Действия
    }

  • Выгрузка плагинов
    - Я советую также поддерживать выгрузку. Это значит удалять сущности, которые ваш плагин создавал или восстанавливать пропатченную память и т.д.
    - Это особенно полезно при тестировании вашего плагина в процессе написания, когда вы постоянно его перезагружаете, например, через плагин AutoReload.
    - Это также предотвратит случаи, когда прикреплённые модели застревают на игроках, потому что они больше не отслеживаются, но и не удаляются.

  • Поддержка Listen серверов - Listen сервера - это когда игровой клиент одновременно является сервером (хостом).
    - Выделенные сервера ВСЕГДА советуют использовать вместо Listen.
    - Индекс 0 на выделенном сервере - это сущность "World". На Listen серверах клиентский индекс через консоль также 0, что создаёт проблемы с командами, используемыми через консоль.

    - Выполнение команд через консоль, которые ожидают получения индекса клиента, часто приводит к ошибкам.
    - Некоторые функции SourceMod не поддерживают listen сервера и ожидают, что индекс клиента будет > 0. Ещё одна причина использовать Выделенные сервера.
    - Некоторый функционал SourceMod также не работает на Listen серверах, к примеру SendConVarValue когда я её тестировал.
    - Просто намотайте на ус: Не используйте Listen сервера!
 
Последнее редактирование:
10. Распространённые ошибки в процессе кодинга:
Writing Sane Plugins - Множество советов и наиболее частые ошибки.​
XY Problem - Нужна помощь? Когда вы задаёте вопрос, убедитесь, что описали реальную проблему, а не то, что по вашему мнению поможет её исправить.​


Ошибки из логов sourcemod/logs/errors_<date>.log:
  • Unable to load plugin (no debug string table) - вам нужно скомпилировать плагин с помощью более старой версии SM, например 1.9 (а лучше обновите версию SourceMod на вашем сервере).

  • Illegal disk size - smx файл повреждён во время загрузки на FTP. Загрузите плагин ещё раз.

  • Client X is not connected или Client X is not in game - потеряна проверка IsClientInGame() в исходном коде.
    - Возможно, недостаточно безопасно хранятся индексы клиентов. См. ниже "Хранение индексов клиентов и сущностей".

  • Invalid entity или Invalid edict - потеряна проверка IsValidEntity() или IsValidEdict() в исходном коде.
    - Возможно, недостаточно безопасно хранятся индексы сущностей. См. ниже "Хранение индексов клиентов и сущностей".

  • "Native XXX is not bound" - зависимость плагина не установлена, не загружена или возникла ошибка при её загрузке.
    - если зависимость опциональна, прежде чем вызывать натив, всегда проверяйте, доступен ли он, используя функцию GetFeatureStatus() в форварде OnAllPluginsLoaded.
    - Либо проверяйте присутствие библиотеки с помощью LibraryExists() в форварде OnAllPluginsLoaded в случае, если зависимость регистрирует такую библиотеку. (Прим. переводчика: вместо LibraryExists лучше использовать пару форвардов OnLibraryLoaded / OnLibraryUnloaded).

  • "Instruction contained invalid parameter" - исходный код был успешно скомпилирован, несмотря на серьезную синтаксическую или структурную ошибку в связи с тем, что парсер не является идеальным. Пример: не-инициализированная переменная.
    - В данном случае, лог ошибки может указывать даже на неверное имя плагина, который не виновен, но приводит к появлению ошибки из-за обращения к другому - виновному плагину.


Ошибки при компиляции:
  • "error 075: input line too long - Файл вероятно имеет неверную кодировку. Преобразуйте её в UTF-8.
    - Альтернативно, продублируйте рабочий скрипт, замените вашим кодом, удалите проблемный файл и переименуйте дубликат в ваше имя файла.

  • Array-based enum structs will be removed in 1.11 / Array-based enum structs have been removed - примеры исправления:
    - через добавление MAX_ITEM в enum: пример
    - через преобразование в enum struct: пример

  • Local variable "v" shadows a variable at a preceding level - означает, что вы объявили переменную "v" дважды.



Ошибки в серверной консоли:
  • Entity 393 (class 'infected') reported ENTITY_CHANGE_NONE but 'm_hEffectEntity' changed. - при изменении некоторых свойств сущности необходимо явно оповещать об этом движок с помощью функции ChangeEdictState. Пример: ChangeEdictState(ent, FindSendPropInfo("infected", "m_hEffectEntity"));



  • Статические переменные - Знайте, когда использовать; предназначены для удержания данных при последующих вызовах функции. Не ожидайте, что такая переменная будет инициализирована со значением по умолчанию при последующем вызове, например, не полагайтесь на то, что строка будет пустой.
    - не рекомендуется для локальных массивов. Пример использования: когда вам не нужна глобальная переменная, можно воспользоваться к примеру статической переменной типа bool для определения, установлен ли хук на функцию, так что вы можете переключать или вызывать функцию множество раз без проблем.
    - если к примеру вы пытаетесь вызвать функцию множество раз в попытке хукнуть что-то и только статический bool оберегает от множественных дублирований хуков, вам вероятно стоит иначе продумать логику скрипта.

  • Если вы пользуетесь транзакциями в базе данных, убедитесь, что создали таблицу с Transaction-Safe типом, например "InnoDB": Руководство.

  • Не используйте FakeClientCommand для вызова вашей зарегистрированной команды в том же самом плагине. Вызывайте сразу по имени колбека, например CmdTest(client, 0);

  • Некоторые люди полагают, что OnPlayerRunCmd использует больше циклов процессора, чем OnGameFrame; это не так. Используйте правильную последовательность проверки; подробности см. ниже.

  • Неверная (не-эффективная) последовательность валидации. Делая битовые операции либо проверку GetGameTime, вы тратите значительно меньше циклов процессора, чем при родных вызовах, таких как IsValidEntity или IsClientInGame. Сначала сделайте первое, прежде чем переходить к более дорогостоящим вызовам. Это особенно важно для хуков Think и OnGameFrame.

  • Неверный порядок проверки клиента. Верным будет: if (client && client <= MaxClients) для проверки на 0 (не поддерживается listen серверами) и клиентского индекса, IsClientConnected (в большинстве случаев не требуется, т.к. следующий вызов покрывает её), IsClientInGame, затем уже всё остальное, например, GetClientTeam, IsPlayerAlive и т.д.

  • Вызов FindConVar множество раз для одного и того же квара. Вместо этого, используйте данную команду в OnPluginStart один раз и сохраняйте возвращенный хендл для последующего использования.

  • Используйте GetEngineVersion вместо GetGameFolderName. Последнее использовалось в старых скриптах до того, как появилась данная функция.

  • Использование strlen для проверки того, что строка пустая. Более быстрый способ - проверить первый знак: blah[0] == 0 или blah[0] == '\0' или !blah[0]

  • Использование только одного события round_end для сброса переменных и т.д. Вы должны учесть, что люди могу сменить карту посреди игры, а в таком случае событие не срабатывает. Для сброса используйте дополнительно OnMapEnd или событие round_start.

  • Вам необходимо проверять, запущена ли карта, прежде чем создавать сущности.

  • Прежде чем искать и изменять сущности в OnMapStart(), вам необходимо подождать немного, пока сущности корректно инициализируются. Лучше всего использовать событие "round_freeze_end" (не доступно в некоторых играх). Или попробуйте воспользоваться RequestFrame. Прим. переводчика: некоторые сущности требуют еще большего времени ожидания, например, машины. Для гарантированного доступа на запись к их свойствам советую подождать первого из событий player_first_spawn, проверив что оно сработало для не-фейкового клиента (т.е. не бота).

  • Отправка большого числа UserMessages или панелей (SendPanelToClient) слишком часто за короткий промежуток времени может спровоцировать краш клиента с ошибкой "Reliable buffer overflow".

  • Меню и панели, а также их пункты не появляются, когда первым знаком строки является "[". Вместо этого используйте " [" (т.е. добавив лидирующий пробел). Учтите, что это может повлиять на случай перечисления в меню имён игроков, которые начинаются на символ квадратной скобки "[".
    - Делайте: ReplaceString(ClientUserName, sizeof(ClientUserName), "[", ""); ИЛИ Format(text_client, sizeof(text_client), " %s", ClientUserName);

Unknown Command: - Потерян return Plugin_Handled;
  • Если вы создаёте команду и получаете "Unknown command" в консоль, вам необходимо завершить колбек через: return Plugin_Handled;

PHP:
public void OnPluginStart()
{
    RegConsoleCmd("sm_test", CmdTest);
}

public Action CmdTest(int client, int args)
{
    // Действия
    return Plugin_Handled;
}

Хранение индексов клиентов и сущностей либо передача их в Таймеры:
  • Чтобы не затронуть неверного клиента, когда он отключается, либо неверную сущность, когда она удаляется, мы используем следующие команды для получения уникального идентификатора для них:
  • GetClientUserId для получения клиентского userid (каждый новый подключаемый клиент будет иметь userid + 1 от предыдущего человека, который зашел на сервер).
  • GetClientOfUserId для обратной операции - получения индекса клиента по его userid. Если он отключился, значение будет 0. Должно использоваться вместе с IsClientInGame. Прим. переводчика: на самом деле, в процессе отключения человек всё еще будет иметь не нулевой индекс, при этом часть команд для такого клиента будут более навалидны. Поэтому проверка IsClientInGame не является опциональной, как могло показаться.
  • EntIndexToEntRef для хранения индекса сущности путем её преобразования в уникальную порядковую ссылку.
  • EntRefToEntIndex для обратной операции - получения индекса сущности из её ссылки. Если сущность более не существует, значение будет равно -1 (INVALID_ENT_REFERENCE).
  • Альтернативой к GetClientUserId и GetClientOfUserId являются: GetClientSerial and GetClientFromSerial.
  • Вот пример хранения, получения и проверки индексов клиента и сущности.

Сущности:
PHP:
public void OnEntityCreated(int entity, const char[] classname)
{
    // Сущность создалась, и её индекс между 0 и 2048 или 4096 для не распространяющихся по сети сущностей.
    if( strcmp(classname "molotov_projectile") == 0 )
    {
        // Получаем ссылку, чтобы избежать случайного влияния на сущность, которая могла бы появится с тем же индексом после удаления нашей
        CreateTimer(5.0, TimerDelete, EntIndexToEntRef(entity));
    }
}

public Action TimerDelete(Handle timer, any entity)
{
    // Преобразовуем ссылку обратно в индекс сущности
    entity = EntRefToEntIndex(entity);

    // Проверяем, что индекс не равен -1 и действителен для использования
    if( entity != INVALID_ENT_REFERENCE )
    {
        RemoveEntity(entity); // Безопасно удаляем эту сущность, зная, что это в точности тот самый исходный оригинал
    } else {
        // Сущность более не существует и мы в безопасности, потому что не повлияем по неосторожности на другую сущность, которая могла бы повторно воспользоваться таким же индексом.
    }
}

Клиенты:
PHP:
int g_iClientList[MAXPLAYERS+1];
public void OnClientPutInServer(int client)
{
    // Всегда передаём UserID в таймеры и проверяем его в колбеке таймера
    CreateTimer(10.0, TimerMessage, GetClientUserId(client));

    // Иной пример: вы могли бы сохранять клиентские UserID в массив, чтобы затем использовав в другой функции проверить, влияем ли мы на тот же самый клиент, что существовал в момент сохранения его UserId.
    g_iClientList[client] = GetClientUserId(client);
}

public Action TimerMessage(Handle timer, any client)
{
    // Получаем клиентский индекс (должен быть в диапазоне от 1 до MaxClients на выделенном сервере)
    client = GetClientOfUserId(client);
    if( client != 0 && IsClientInGame(client) )
    {
        // Это тот же самый клиент, на который мы хотим повлиять
    } else {
        // Должно быть он отключился
    }
}

// Другой пример, допустим, вызываем функцию, которая использует сохранённый ранее список клиентских UserID:
public void SomeFunction(int client)
{
    int client = GetClientOfUserId(g_iClientList[client]);
    if( client && IsClientInGame(client) )
    {
        // Это тот же самый клиент, что и был ранее (при сохранении списка UserId)
    } else {
        // Должно быть он отключился
    }
}

OnEntityCreated - Получение данных:
  • Попытка получить имя модели в форварде OnEntityCreated может не сработать. Правильный способ - использование SDKHooks SpawnPost или RequestFrame.
  • Некоторые сущности всё ещё не будут иметь корректных данных о позиции или скорости. Вам необходимо также воспользоваться SpawnPost и возможно, дополнительно RequestFrame после него.

PHP:
public void OnEntityCreated(int entity, const char[] classname)
{
    if( strcmp(classname "molotov_projectile") == 0 )
    {
        SDKHook(entity, SDKHook_SpawnPost, SpawnPost);
    }
}

public void SpawnPost(int entity)
{
    // Верификация
    if( !IsValidEntity(entity) ) return;

    // Теперь модель действительна
    char sModel[64];
    GetEntPropString(entity, Prop_Data, "m_ModelName", sModel, sizeof(sModel));

    // В зависимости от сущности, скорость может быть некорректна до следующего фрейма
    RequestFrame(nextFrame, EntIndexToEntRef(entity)); // Вторым параметром передаём переменную в колбек. Она опциональна, и в этом примере мы её используем.
}

public void nextFrame(int entity)
{
    // Верифицируем
    if( (entity = EntRefToEntIndex(entity)) != INVALID_ENT_REFERENCE )
    {
        // Теперь данные о скорости корректны
        float vVel[3];
        GetEntPropVector(entity, Prop_Send, "m_vInitialVelocity", vVel);
    }
}

Объявление переменных:
  • Объявляйте переменные за пределами цикла. Указание их внутри является очень неэффективным, особенно для строк. К примеру:

PHP:
// Это правильно:
    char temp[64];
    int test;
    for( int i = 1; i < 2048; i++ )
    {
        // Прочее
    }

// Это плохо:
    for( int i = 1; i < 2048; i++ )
    {
        char temp[64];
        int test;
        // Прочее
    }

Смешивание разных типов переменных (int вместе с float):
PHP:
// Неверно:
float f = 1.5;
int i = 2;
i += f;
PrintToServer("wrong: %i", i); // Ответ: 1080033280

// Правильным будет сперва привести float в int через округление:
float f = 1.5;
int i = 2;
i += RoundToCeil(f); // или RoundToFloor, RoundToZero, RoundFloat, RoundToNearest
PrintToServer("correct: %i", i); // Ответ: 4
 
Последнее редактирование:
11. Отладка плагинов:
Инфа от Dragokas:​

  • Периодически проверяйте логи addons/sourcemod/logs/error_<date>.log; наиболее важные части, как номер строки и описательная часть ошибки, часто пропускаются пользователями, когда они запрашивают помощь.

  • Наиболее прямой и быстрый способ разрешения конфликтов между плагинами либо поиска проблемного плагина - это половинное деление, путём удаления половины плагинов -> тестирования -> повторения метода снова

  • Установите Расширение Accelerator (лучше всего работает под Linux), которое предоставит различную отладочную инфу, когда сервер крашится.

  • Используйте ключ -debug в стартовых параметрах командной строки запуска сервера для получения более детального описания в краш логах.
  • Используйте ключ -condebug для сохранения вывода серверной консоли в лог console.log, хранящегося в корневой папке сервера.

  • Утечки памяти можно найти, используя команду sm_dump_handles handles.txt и изучая результаты. Также, хороший онлайн сумматор для анализа дампа: Handle Dump parser
    Обращайте внимание на сущности со слишком большим количеством дескрипторов (handles), к примеру, > 50 (кроме форвардов), а не на общий объем потребляемой ими памяти, как почему-то делают некоторые люди при анализе, если только вы не автор данного плагина, чтобы делать заключения на этом основании.
  • Во втором случае лучше сравнивать память по временной линии, чтобы понять утечка это или нет:
    Небольшое пояснение - сделайте дамп #1, подождите до момента перед окончанием карты, сделайте дамп #2, смените карту, сделайте дамп #3; перезагрузите сервер, сделайте дамп #4, последовательно сравните все снимки (например, через WinMerge), выделив наиболее подозрительные значения, чтобы увидеть на сколько сильно гуляет кол-во и объем памяти/хендлов в разное время. В большинстве случаев, значения не будут отличаться, либо будут, но в очень небольших пределах.

  • Используйте плагин Valve Profiler для измерения производительности каждого плагина в разрезе отдельных функций, используя профайлер Valve. В отчёте также будет стек вызова функций плагинов и игры, а также графики с сортировками по макс. кол-ву вызовов, макс. времени, затраченном на вызов и т.п.

  • Функция LogStackTrace - полезна для записи цепочки стека вызова функций в лог ошибок. Эта функция доступна, начиная с SM 1.10.

  • Пример кода оценки производительности плагина (бенчмаркинг):
    - Ещё я запускаю sm_test несколько раз для получения более усреднённого результата. Иногда минимальное и максимальное значения весьма отличаются.
PHP:
#include <profiler>
public void OnPluginStart()
{
    RegAdminCmd("sm_test", sm_test, ADMFLAG_ROOT);
}

public Action sm_test(int client, int args)
{
    Handle hProf = CreateProfiler();
    float min = 10.0, max, avg;

    // Запуск теста 20 раз для получения min/max/avg
    for( int x = 0; x < 20; x++ )
    {
        StartProfiling(hProf);

        // Запуск 1000 итераций для нашего теста
        for( int i = 0; i < 1000; i++ )
        {
            // Вставьте сюда код функции, которую желаете протестироваать
        }

        StopProfiling(hProf);

        float speed = GetProfilerTime(hProf);
        if( speed < min ) min = speed;
        if( speed > max ) max = speed;
        avg += speed;
    }

    // Отображает минимальное, максимальное и среднее значения.
    avg /= 20;
    PrintToServer("Bench: Min %f. Avg %f. Max %f", min, avg, max);
    delete hProf;
}
 
Последнее редактирование:
12. Компиляция плагинов:
  • Compiling SourceMod Plugins - Руководство с различными методами локальной компиляции.
  • Online SourceMod Compiler - Компиляция через браузер путём копирования-вставки кода; сторонние inc файлы не поддерживаются.
  • Limetech Compiler by asherkin - Компиляция через браузер путём копирования-вставки кода, разрешены сторонние include файлы.

  • Загрузка плагинов:
    - Как загружать плагины без перезапуска сервера.
    - Также, можно воспользоваться командой: sm plugins refresh
    - Плагин DevCmds предоставляет команды: sm_refresh для обновления плагинов и sm_renew для перезагрузки их всех сразу.


13. Публикация плагинов:
Рекомендации при публикации темы с новым плагином.​
Writing Sane Plugins - Множество советов и часто встречающиеся ошибки.​
Вы будете удивлены, на сколько часто люди спрашивают самую элементарную информацию, даже если для вас она кажется очевидной и поэтому не указывается в вашей теме.​
1. Предоставляйте информативное описание о функционале плагина и о том, как им пользоваться.​
2. Перечисляйте все команды, квары и имя файла конфига с кварами, а также его расположение.​
3. Объясняйте, как установить плагин, включая указание путей к дополнительным файлам настроек, моделей, звуков и т.п.​
4. Перечисляйте требования и оставляйте гиперссылки на зависимости, без которых ваш плагин не сможет заработать, такие как инклуды, другие плагины и расширения.​
5. Загружайте заранее скомпилированный .SMX только в том случае, если вы используете сторонние инклуды, из-за которых форумный движок (AM) не сможет самостоятельно их скомпилировать при нажатии на ссылку "Get Plugin".​
6. Используйте знаки подчёркивания "_" в именах файлов. Пробелы " " усложняют загрузку плагина вручную, а при скачивании в их именах появляются знаки "%20" на месте пробелов. (Прим. переводчика: вы всё ещё можете взаимодействовать с такими плагинами из консоли, окружив имя кавычками, например, sm plugins reload "my plugin.smx").​
7. Также, не используйте такие знаки, как "[", "]" или "&" в именах файлов, т.к. они отображаются в виде "%5B", "%5D" и "%26" при загрузке с AlliedMods.​



Благодарности:
- разработчикам SourceMod и MetaMod за их великолепную работу.​
- авторам SourceMod wiki - здесь приведены ссылки на множество их статей.​
- авторам плагинов и сообществу SourceMod за множество плагинов и ссылок в этой статье.​
- Dragokas за тонну дополнений в этой теме и множество рекомендаций!​
- Maxximou5, Marttt, zipcore, Desktop, MAGNAT2645, xZk, asherkin, JoinedSenses, Lux, GAMMACASE, SM9(); и CrazyHackGUT за их вклад в эту тему.​



Список изменений:

10.01.2021​
- перевод на русский язык​
- множество правок и дополнений​
- добавлен раздел "Терминология"​
- обновлён раздел "Подключение к серверу"​
- Распространённые ошибки разделены на группы​
- Добавлен пункт о "ChangeEdictState"​
- добавлен ключ -condebug​
- дополнен SetTransmit (инфа от asherkin)​
- добавлены команды report_entities, cl_showents​
- релизнуто обновление для ac_debug (valve profiler), теперь с поддержкой L4D1/2​
- Исправлен упущенный OnMapEnd() в части примеров "Как остановить (удалить) глобальный таймер"​
- Плагин Remove Weapons/Carryables Collision добавлен в список рекомендованных фиксов для L4D1/2​
- Плагин Return To Lobby Fix добавлен в список рекомендованных фиксов для L4D1/2​
- Плагин Cvar and Command List добавлен в список "Для разработчиков"​
- Плагин Entity Limits Logger добавлен в список "Для разработчиков"​
- Плагин ac_debug заменён на новый релиз Valve Profiler
- Скрипт [VBS] Simple Regexp Syntax updater добавлен в "Прочие инструменты"​
- Скрипт MethodMapize добавлен в "Прочие инструменты"​
- Добавлена ссылка на статью: "Как сделать модель компактной" (в "Прочие инструменты")​
 
Последнее редактирование:
Дополнения к статье:


Другие инструменты
Инфа от Dragokas

Вот набор инструментов, которые использую лично я:

VPK:
- GCFScape - Распаковка VPK
- vpk.exe (в ресурсах игры) - упаковка в VPK, просто перетащите папку на него

Карты и декомпиляция:
- pakrat - декомпилятор
- bspsrc - декомпилятор
- VIDE - комбайн, работа с BSP, LMP, VMT, VTF, партиклями...

Скины:
- VTFEdit - изучение vtf, конвертирование, создание спреев...
- Crowbar - декомпиляция / компиляция модели, интеграция с Model Viewer
- [Пакетная] упаковка/распаковка/проверка архивов BZ2
- [Пакетная] Проверка потерянных текстур и "сжатие" модели
- WinHEX :)
- SkyPaint - создание скайбоксов
- VMT Editor - пакетная работа с VMT, также используется для скайбоксов
- См. также мою статью "Как сделать модель компактной"

Серверное:
- FTP Uploader - загрузка плагина на FTP, как только изменился его штамп времени
- Double click compiler - авто-компиляция плагина по двойному клику (с поддержкой хоткеев для N++/AkelPad/Sublime)
- HLSW - выполнение RCON команд

Работа с SMX и исходниками:
- Spider - компилятор через браузер с поддержкой сторонних *.inc
- WinMerge (бесплатен), Beyond Compare (trial) - сравнение различий между исходниками двух плагинов
- [VBS] Simple Regexp Syntax updater - авто-обновление старого синтаксиса исходников с помощью набора правил RegExp. См. также.
- MethodMapize - авто-обновление SP кода для использования API методмапов
- Lysis - для декомпиляции - когда, например, вы не помните, что это за плагин установлен, либо не знаете что это за версия, либо не можете найти исходник
- spcomp.exe - снимки других версий (1.10, 1.9, 1.8, 1.7) - полезны при анализе незнакомой версии smx известного плагина
Это когда у вас на руках есть неизвестная версия smx, а также исходный код любой версии этого же smx.
Алгоритм будет таков:
  • используйте декомпилятор, что посмотреть в заголовке версию компилятора, с которым был собран неизвестный smx
  • используйте ту же версию компилятора, чтобы создать наиболее похожий по структуре smx из последнего доступного исходного кода
  • декомпилируйте ваш собственный только что собранный smx
  • декомпилируйте неизвестную версию smx
  • сравните эти результаты в текстовой режиме (например, в WinMerge)
 
Последнее редактирование:
Минимальный пример "Как остановить (удалить) глобальный таймер"
Инфа от Dragokas

Я вижу, как очень часто задают вопросы об ошибках:
- Native "KillTimer" reported: Invalid timer handle XXX (error 3)
- Handle XXX is invalid (error 1)
- TIMER_FLAG_NO_MAPCHANGE не останаливает таймер, когда раунд закончен/проигран и т.д.
  • когда кто-то желает запустить одноразовый или повторяющийся таймер, а затем остановить его в round_end вручную, или с использованием флага TIMER_FLAG_NO_MAPCHANGE.
Чтобы сделать это без ошибок, вам необходимо создавать таймер правильно и обнулять глобальную переменную таймера в нужных местах.

Вот минимальные примеры с комментариями, как это грамотно сделать:

1. Единоразовый таймер с хендлом в глобальной переменной (timer_single.sp)
PHP:
#include <sourcemod>

Handle g_iTimer; // глобальная переменная нашего таймера

public void OnPluginStart()
{
    HookEvent("round_start",             Event_RoundStart,         EventHookMode_PostNoCopy);
    HookEvent("round_end",                 Event_RoundEnd,         EventHookMode_PostNoCopy);
}

// Давайте создадим таймер в начале раунда:

public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
    delete g_iTimer; // удалим предыдущий таймер на случай, если событие "round_start" сработает дважды

    g_iTimer = CreateTimer(30.0, Timer_Sample);

    // не ставьте здесь TIMER_FLAG_NO_MAPCHANGE !!!
    // потому что переменная с хендлом таймера не будет обнулена автоматически в случае, если произойдёт смена карты прежде, чем сработает таймер
}

public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
    delete g_iTimer; // останавливаем таймер на случай, если он ещё не сработал, таким образом этот старый таймер не будет перенесён на новый раунд и не сработает случайно в процессе загрузки уровня

    // ключевое слово "delete" останавливает таймер и автоматически присваивает ноль переменной.
    // Заметка: "delete 0" не вызывает срабатывания исключения.
}

public void OnMapEnd() // required, because forcible change level doesn't fire "round_end" event
{
    delete g_iTimer;
}

public Action Timer_Sample(Handle timer)
{
    // делаем что-либо
    PrintToChatAll("tick");

    // не забываем обнулить здесь переменную-хендл таймера, т.к. после завершения колбека этот хендл станет недействительным
    g_iTimer = null;
}

2. Повторяющийся таймер с хендлом в глобальной переменной (timer_repeat.sp)
PHP:
#include <sourcemod>

Handle g_iTimer;

public void OnPluginStart()
{
    HookEvent("round_start",             Event_RoundStart,         EventHookMode_PostNoCopy);
    HookEvent("round_end",                 Event_RoundEnd,         EventHookMode_PostNoCopy);
}

public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
    // Давайте создадим таймер в начале раунда:

    delete g_iTimer; // предотвращает двойной запуск (на всякий случай)

    // не ставьте сюда флаг TIMER_FLAG_NO_MAPCHANGE !!!
    // потому что переменная с хендлом не будет обнулена
    g_iTimer = CreateTimer(1.0, Timer_Sample, _, TIMER_REPEAT);
}

public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
    delete g_iTimer;
    // ключевое слово "delete" останавливает таймер и автоматически присваивает ноль переменной.
    // Заметка: "delete 0" не вызывает срабатывания исключения.
}

public void OnMapEnd() // required, because ForceChangeLevel doesn't cause "round_end" event
{
    delete g_iTimer;
}

public Action Timer_Sample(Handle timer)
{
    // полезная нагрузка
    PrintToChatAll("tick");

    return Plugin_Continue;

    /*
    Замечание 1: если вы желаете принудительно остановить таймер здесь, вам необходимо сначала присвоить ноль переменной с хендлом таймера

    g_iTimer = null;
    return Plugin_Stop;

    Замечание 2: Никогда не пытайтесь применить delete или KillTimer() на хендле таймера внутри своего же колбека (т.е. здесь).
    */
}

3. Единоразовый таймер с хендлами для каждого клиента в глобальном массиве (timer_single_per-client.sp)
PHP:
#include <sourcemod>

Handle g_iTimer[MAXPLAYERS+1];

public void OnPluginStart()
{
    HookEvent("round_start",             Event_RoundStart,         EventHookMode_PostNoCopy);
    HookEvent("round_end",                 Event_RoundEnd,         EventHookMode_PostNoCopy);

    RegConsoleCmd("sm_timer", CmdTestTimer, "Re-starts timer for testing")
}

Action CmdTestTimer(int client, int args)
{
    delete g_iTimer[client];
    g_iTimer[client] = CreateTimer(15.0, Timer_Sample, GetClientUserId(client));
}

public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
    for ( int i = 1; i <= MaxClients; i++ )
    {
        if ( IsClientInGame(i) )
        {
            // запускаем одиночный таймер для каждого клиента отдельно
            delete g_iTimer[i];
            g_iTimer[i] = CreateTimer(3600.0, Timer_Sample, GetClientUserId(i));
        }
    }
}

public void OnClientDisconnect(int client)
{
    // детальное объяснение см. в следующем примере
    delete g_iTimer[client];
}

void Reset()
{
	for ( int i = 1; i <= MaxClients; i++ )
	{
		delete g_iTimer[i];
	}
}

public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
	Reset();
}

public void OnMapEnd()
{
	Reset();
}

public Action Timer_Sample(Handle timer, int UserId)
{
    int client = GetClientOfUserId(UserId);

    if ( client && IsClientInGame(client) )
    {
        g_iTimer[client] = null;
  
        // что-нибудь делаем
        // ...
        PrintToChat(client, "tick");
    }
}

4. Повторяющийся таймер с хендлами для каждого клиента в глобальном массиве (timer_repeat_per-client.sp)
PHP:
#include <sourcemod>

Handle g_iTimer[MAXPLAYERS+1];

public void OnPluginStart()
{
    HookEvent("round_start",             Event_RoundStart,         EventHookMode_PostNoCopy);
    HookEvent("round_end",                 Event_RoundEnd,         EventHookMode_PostNoCopy);

    RegConsoleCmd("sm_timer", CmdTestTimer, "Starts timer for testing")
}

Action CmdTestTimer(int client, int args)
{
    // останавливаем таймер, если он уже был запущен
    // это также установит значение переменной в 0
    delete g_iTimer[client];
    PrintToChatAll("timer = %i", g_iTimer[client]);
    // запускаем повторяющийся таймер
    // передаём ссылку на клиента, таким образом мы сможем убедиться, что он не подменён за время, пока таймер на самом деле не сработает
    g_iTimer[client] = CreateTimer(15.0, Timer_Sample, GetClientUserId(client), TIMER_REPEAT);
}

public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
    for ( int i = 1; i <= MaxClients; i++ )
    {
        if ( IsClientInGame(i) )
        {
            // запускаем повторяющийся таймер для каждого клиента
            delete g_iTimer[i];
            g_iTimer[i] = CreateTimer(15.0, Timer_Sample, GetClientUserId(i), TIMER_REPEAT);
        }
    }
}

public void OnClientDisconnect(int client)
{
    // убедимся, что убили таймер при отключении клиента
    delete g_iTimer[client];
}

void Reset()
{
	for ( int i = 1; i <= MaxClients; i++ )
	{
		delete g_iTimer[i];   // останавливаем таймер для каждого игрока
	}
}

public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
    Reset();
}

public void OnMapEnd() // требуется, потому что ForceChangeLevel не вызывает события "round_end"
{
	Reset();
}

public Action Timer_Sample(Handle timer, int UserId)
{
    // проверяем, является ли тем же клиентом
    int client = GetClientOfUserId(UserId);

    // проверка, действителен ли
    if ( client && IsClientInGame(client) )
    {
        // что-нибудь делаем
        // ...
        PrintToChat(client, "tick");
        return Plugin_Continue;
  
        /*     или, если вам хочется остановить таймер
            но, не забудьте сперва обнулить ('null') таймер:
  
        g_iTimer[client] = null;
        return Plugin_Stop;
        */
    }
    // здесь вы не сможете обнулить переменную таймера, потому что вы не знаете правильный индекс клиента для доступа к массиву хендлов, а поскольку клиент уже мог выйти, то GetClientOfUserId не сработает
    // поэтому вместо этого мы используем форвард OnClientDisconnect()
    return Plugin_Stop;
}

5. (Без глобальной переменной) Приостанавливаем повторяющийся таймер
- без необходимости управлять хендлом в глобальной переменной (timer_repeat_pause.sp)
PHP:
/*
    Этот пример демонстрирует 'Шлепок игрока' через 30.0 сек. задержки от начала раунда и повторяет каждые 2 секунды, пока не закончится раунд.

    Спасибо Marttt за идею и код.
*/

#pragma semicolon 1
#pragma newdecls required

#include <sourcemod>
#include <sdktools>

#define WAIT_TIME 30.0

float g_fRoundStartTime;

public void OnPluginStart()
{
    HookEvent("round_start",             Event_RoundStart,         EventHookMode_PostNoCopy);
    HookEvent("round_end",                 Event_RoundEnd,         EventHookMode_PostNoCopy);

    // запускаем таймер, который никогда не будет уничтожен
    CreateTimer(2.0, Timer_CheckPlayerZone, _, TIMER_REPEAT);
}

public Action Event_RoundStart(Handle event, const char[] name, bool dontBroadcast)
{
    g_fRoundStartTime = GetEngineTime();
}

public Action Event_RoundEnd(Handle event, const char[] name, bool dontBroadcast)
{
    g_fRoundStartTime = 0.0;
}

public void OnMapEnd()
{
    g_fRoundStartTime = 0.0;
}

public Action Timer_CheckPlayerZone(Handle timer)
{
    if ( g_fRoundStartTime == 0.0 ) // "приостанавливаем" таймер между событиями "round_end" - "round_start"
        return Plugin_Continue;

    if ( GetEngineTime() - g_fRoundStartTime < WAIT_TIME ) // "приостанавливаем" таймер на протяжении 30.0 сек. после "round_start"
        return Plugin_Continue;

    for ( int i = 1; i <= MaxClients; i++ )
    {
        if ( IsClientInGame(i) && IsPlayerAlive(i) )
        {
            SlapPlayer(i, 20, true);
            PrintToChat(i, "You are being slapped!");
        }
    }
    return Plugin_Continue;
}

-------------------

Общие советы:
  • Не используйте флаг TIMER_FLAG_NO_MAPCHANGE, если вы присваиваете хендл таймера глобальной переменной с целью дальнейшего контроля таймера через эту переменную позднее.
    - Флаг TIMER_FLAG_NO_MAPCHANGE может быть безопасно использован, если вы создаёте таймер без назначения его хендла переменной.

  • Когда вы убиваете таймер за пределами колбека этого таймера, используйте ключевое слово 'delete'.

  • Когда вы убиваете повторяющийся таймер внутри его же колбека, используйте исключительно "return Plugin_Stop"!
    - Не используйте там "delete" или"KillTimer()", иначе можете получить ошибки вида:
    Plugin "XXX.smx" encountered error 23: Native detected error
    [SM] Invalid timer handle XXX (error 3) during timer end, displayed function is timer callback, not the stack trace

  • Вы должны всегда присваивать ноль глобальной переменной таймера в самой последней строке колбека таймера (перед return) в момент перед разрушением хендла, который наступает:
    - для повторяющегося таймера, - когда вы передаёте "return Plugin_Stop"
    - для единоразового таймера, - автоматически, как только завершается колбек таймера.

    Если у вас хендлы таймера хранятся в глобальном массиве и вы не знаете, как найти нужный хендл в массиве из-под колбека таймера, то:
    - в таймерах вида "для каждого игрока", используйте форвард OnClientDisconnect(), чтобы получить индекс массива.
    - в таймерах вида "авто-инкремент":
таких как:
PHP:
const int MAX = 100;

Handle g_iTimer[MAX];

int g_iTimerCount;

public void OnPluginStart()
{
    g_iTimer[g_iTimerCount++] = CreateTimer( 1.0, Timer_Sample );
    g_iTimer[g_iTimerCount++] = CreateTimer( 2.0, Timer_Sample );
}

для обнуления глобальной переменной (элемента массива) из-под колбека таймера вы можете воспользоваться вспомогательной функцией:
PHP:
void NullifyHandle(Handle timer)
{
    for( int i = 0; i <= g_iTimerCount; i++ )
    {
        if( timer == g_iTimer[i])
        {
            g_iTimer[i] = null;
            break;
        }
    }
}

Использование:
PHP:
public Action Timer_Sample(Handle timer)
{
    // some code
    // ...

    NullifyHandle(timer); // найдёт соответствие хендла в массиве и обнулит его
    return Plugin_Stop;
}

  • Удаляйте таймер перед его созданием, чтобы избежать случайного его срабатывания дважды; некоторые события, такие как "round_start" имеют привычку вызываться несколько раз (в определённых играх).

  • Чтобы остановить таймер по окончании раунда, используйте оба, и событие "round_end", и форвард "OnMapEnd()", потому что "round_end" не срабатывает, когда карта сменяется принудительно.
 

Вложения

Последнее редактирование:
К слову о поточных операциях с базой данных (в формате вопрос-ответ)
Спасибо за предоставление инфы Kruzya.

aleeexxx написал(а):
Можете ли вы добавить советы о много-поточных функциях для SQL
Там нет так называемых много-поточных функций.

Существуют только:
  • не-поточные - выполняются в том же потоке, что и плагины SM, таким образом каждый запрос к БД приводит к ожиданию ответа и остановке выполнения операций в других плагинах на протяжении этого времени.
  • поточные - эти выполняют запросы к БД в другом, втором потоке; когда вы выполняете ещё один запрос параллельно, новые потоки больше не создаются, запрос становится в очередь в тот же самый второй поток.

aleeexxx написал(а):
Я только недавно узнал, что старые функции могут блокировать основной поток, а новый синтаксис уже выполняет в много-поточной манере.
Поправьте меня, если я не прав!

Не все из них.
Некоторые из функций методмапа Database всё ещё указывают на старые не-поточные функции в данный момент.

Пример: Database.SetCharset() - это просто алиас SQL_SetCharset, см. исходник.

Но есть и некоторые исключения.
Например: Database.Escape() - даже несмотря на то, что согласно исходнику, он обращается к хендлу БД, при этом внешний запрос не выполняется. Вместо этого, экранирование выполняет сам клиент, основываясь на предварительно за-кешированной кодировке соединения.

Дополнительную инфу можно получить в этом руководстве от R1KO.
А также будет полезным почитать там комментарии.
 
Последнее редактирование:
Зарезервировано.
 
Последнее редактирование:
Статус
В этой теме нельзя размещать новые ответы.
Назад
Сверху Снизу