SourceMod Plugin Approver
Join Date: Aug 2010
Location: SpaceX
|
01-24-2020
, 18:39
[TUT] SourcePawn Scripting - Tips, Basics to Advanced
|
#1
|
Please help contribute to this thread and recommend changes, links and information.
Table of Contents:
1. SourceMod Basics and Resource Links
2. Server Setup
3. Connect to Server
4. Scripting Environments
5. Useful Plugins + Exts for Developers
6. Server Fixes - (plugins + extensions)
7. Various Functions and Information
8. Advanced Techniques
9. Good Coding Practices
10. Common Scripting Mistakes
11. Debugging Plugins
12. Compiling Plugins
13. Releasing Plugins
1. SourceMod Basics and Resources:
I want to emphasize how important looking at many different plugins and examples can be to help you learn.
Use find to search for cvars and commands by entering "find <keyword>" into your server or client console. Both may return different results, e.g. server and client specific cvars.
Beginners to scripting:
Useful Resources:
Russian speaking community:
2. Server Setup:
Steam CMD:
- SteamCMD is used to download, install and update servers. Download and install SteamCMD before installing the server.
Example Windows .bat file to install and update a CS:GO server. Other servers can be setup by replacing the APP_ID number from the list found here.
Spoiler
CS:GO install and update:
PHP Code:
@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
Example Windows .bat file to start a CS:GO server which automatically starts on de_dust2 map.
Spoiler
CS:GO start:
PHP Code:
@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%
Example Windows .bat file to start an L4D2 server and display a list of options to select which map the server starts on.
Spoiler
L4D2 start:
PHP Code:
@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
Saving disk space:
I run my game client and server on the same PC. To save disk space I used SymLinking to point a bunch of server files to my client files.
This means whenever my client updates part of the server files may update, so make sure to update the server. Updates may also add new .VPK files and you might want to SymLink those too.
There seems to an issue when updating the server. It re-downloads some files and replaces the SymLink shortcuts. For games that are often updated it might not be so good.
Example Windows .bat saves ~14.8gb for CS:GO and ~8.22gb + 10,200 files for L4D2:
Spoiler
CS:GO SymLink:
PHP Code:
@echo off
REM Saves ~14.8gb
REM Change these folder paths. This SymLink will point server files to client files.
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 SymLink:
PHP Code:
@echo off
REM Change these folder paths. This SymLink will point server files to client files.
set "SERVER=C:\Servers\L4D2"
set "CLIENT=C:\Steam\SteamApps\common\left 4 dead 2"
REM ~5.77 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.45 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_015.vpk" "%CLIENT%\left4dead2\pak01_015.vpk"
mklink "%SERVER%\left4dead2\pak01_016.vpk" "%CLIENT%\left4dead2\pak01_016.vpk"
mklink "%SERVER%\left4dead2\pak01_017.vpk" "%CLIENT%\left4dead2\pak01_017.vpk"
mklink "%SERVER%\left4dead2\pak01_018.vpk" "%CLIENT%\left4dead2\pak01_018.vpk"
mklink "%SERVER%\left4dead2\pak01_019.vpk" "%CLIENT%\left4dead2\pak01_019.vpk"
mklink "%SERVER%\left4dead2\pak01_020.vpk" "%CLIENT%\left4dead2\pak01_020.vpk"
mklink "%SERVER%\left4dead2\pak01_021.vpk" "%CLIENT%\left4dead2\pak01_021.vpk"
mklink "%SERVER%\left4dead2\pak01_022.vpk" "%CLIENT%\left4dead2\pak01_022.vpk"
mklink "%SERVER%\left4dead2\pak01_023.vpk" "%CLIENT%\left4dead2\pak01_023.vpk"
mklink "%SERVER%\left4dead2\pak01_024.vpk" "%CLIENT%\left4dead2\pak01_024.vpk"
mklink "%SERVER%\left4dead2\pak01_025.vpk" "%CLIENT%\left4dead2\pak01_025.vpk"
mklink "%SERVER%\left4dead2\pak01_026.vpk" "%CLIENT%\left4dead2\pak01_026.vpk"
mklink "%SERVER%\left4dead2\pak01_027.vpk" "%CLIENT%\left4dead2\pak01_027.vpk"
mklink "%SERVER%\left4dead2\pak01_028.vpk" "%CLIENT%\left4dead2\pak01_028.vpk"
mklink "%SERVER%\left4dead2\pak01_029.vpk" "%CLIENT%\left4dead2\pak01_029.vpk"
mklink "%SERVER%\left4dead2\pak01_030.vpk" "%CLIENT%\left4dead2\pak01_030.vpk"
mklink "%SERVER%\left4dead2\pak01_031.vpk" "%CLIENT%\left4dead2\pak01_031.vpk"
mklink "%SERVER%\left4dead2\pak01_032.vpk" "%CLIENT%\left4dead2\pak01_032.vpk"
mklink "%SERVER%\left4dead2\pak01_033.vpk" "%CLIENT%\left4dead2\pak01_033.vpk"
mklink "%SERVER%\left4dead2\pak01_034.vpk" "%CLIENT%\left4dead2\pak01_034.vpk"
mklink "%SERVER%\left4dead2\pak01_035.vpk" "%CLIENT%\left4dead2\pak01_035.vpk"
mklink "%SERVER%\left4dead2\pak01_036.vpk" "%CLIENT%\left4dead2\pak01_036.vpk"
mklink "%SERVER%\left4dead2\pak01_037.vpk" "%CLIENT%\left4dead2\pak01_037.vpk"
mklink "%SERVER%\left4dead2\pak01_dir.vpk" "%CLIENT%\left4dead2\pak01_dir.vpk"
pause
MetaMod and SourceMod installation:
- To run plugins you need to install MetaMod and SourceMod.
- It's recommended to keep these updated to the latest stable release to receive any security patches, performance increases and new features.
- Managing your Sourcemod installation - details about SM folder layout, installing plugins and extensions.
Other references:
3. Connect to Server:
Info by Dragokas:
To allow other people join your server over the Internet:
- Setup port forwarding for 27016 port (or whichever port your game server uses).
- Configure firewall to allow same port (tcp/udp). More details.
- (CS:GO only) Create Token and specify it in csgo\cfg\autoexec.cfg file, set: sv_setsteamaccount <login_token>
- Other people should enter in console:
Code:
connect Your.Public.IP:27016
If YOU want to join a local dedicated server:
- Your client application should be run before the server! (otherwise, Steam will deny you from running the client; so, later you could only run it by executing .exe file directly)
- Enter in client's console:
Code:
connect Local.IP:27016
4. Scripting Environments:
- Everyone has different editor preferences. Here are some examples:
- Please link more.
SourcePawn specific editors:
- BasicPawn - "lightweight and basic SourcePawn Editor that uses dynamic Autocompletion and IntelliSense"
- SPEdit - "is an Editor for Sourcepawn with several features to make the coding in sp easy for you"
- SPCode - fork of SPEdit which aims to fix issues and add some small features
Text editors:
- NotePad++
- NotePad++ - Supports various programming languages, SourceMod community has made custom modifications for SourcePawn:
- Setting up Notepad++ for SourceMod.
- I personally use this, syntax highlighting, functions auto completion, keybind to compile and move plugins to the server and displays warnings or errors allowing double click to goto the problem line.
- I use the following with NPP:
Spoiler
Plugins:
- Compare
- DSpellCheck
- TextFX Characters
- NppExec
NppExec script:
- I use this to compile and move plugins to various games depending which I am testing for. By putting // in front of "set GAME" it will compile and move to different games. e.g. this will compile and move to the CS:GO folder. It doesn't matter where the .sp script is saved.
PHP Code:
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
5. Useful Plugins and Extensions for Developers:
6. Server Fixes - (plugins + extensions for various games):
ANY:
CS:S:
CS:GO:
L4D1:
L4D2:
7. Various Functions and Information:
Hammer and Authoring Tools:
Particles:
Spoiler
- In Hammer (L4D2 at least) you can view particles by placing an "info_particle_system" entity and double clicking "Particle System Name" in the keyvalues list. This will open the "Particle Browser". Particle Editor through "L4D Authoring Tools" -> L4D (Tools Mode).
- There is also a [Batch] Particles Extractor & Tester plugin and guide by Dragokas.
- Some or most source games also include an in-game Particle Editor (see link on how to enable), useful for viewing and creating custom particles. You can use SourceMod's AddFileToDownloadsTable function to send your custom models, sounds, particles etc to clients.
- To precache particles use the following stock and call in OnMapStart();
PHP Code:
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: (custom content)
Entity Properties: (Prop_Data and Prop_Send)
Events:
Sound Hooks:
Spoiler
- Sound Hooks can change volume, sample file or block sounds.
PHP Code:
public void OnPluginStart()
{
// WARNING: Avoid hooking more than once, only hook once.
// You could for example use a static or global bool e.g. g_bHooked to avoid multiple sound hooks.
// You can hook whenever you need, it doesn't have to be in OnPluginStart.
// Most sounds play through here
AddNormalSoundHook(SoundHook);
// Some sounds might only play through here
AddAmbientSoundHook(AmbientHook));
}
public void OnPluginEnd()
{
// You don't need to unhook in OnPluginEnd, just showing you can unhook when not required.
RemoveNormalSoundHook(SoundHook);
RemoveAmbientSoundHook(AmbientHook);
}
// Watch for thrown grenades.
public void OnEntityCreated(int entity, const char[] classname)
{
if( strncmp(classname, "molotov_projectile", 13) == 0 )
{
SDKHook(entity, SDKHook_SpawnPost, SpawnPost);
}
}
void SpawnPost(int entity)
{
RequestFrame(OnNextFrame);
g_bBlockSound = true;
}
void OnNextFrame()
{
g_bBlockSound = false;
}
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)
{
// Since the SoundHook is called for every sound played, it's better to avoid string checking when you can
if( g_bBlockSound )
{
// Block molotov sound when throwing.
if( strcmp(sample, "weapons/molotov/fire_loop_1.wav") == 0 )
{
return Plugin_Handled;
}
}
// Reduce explosion volume
if( strcmp(sample, "weapons/molotov/molotov_detonate_3.wav") == 0 )
{
volume = 0.5;
return Plugin_Changed;
}
// Nothing happened
return Plugin_Continue;
}
Action AmbientHook(char sample[PLATFORM_MAX_PATH], int &entity, float &volume, int &level, int &pitch, float pos[3], int &flags, float &delay)
{
// Same as above but for Ambient sounds.
}
SetTransmit: (Hiding models from certain players):
Spoiler
- SDKHook_SetTransmit can be used to hide models, particles and some other entity types (e.g. light_dynamic) from clients.
- Hiding particles might not work for all games or might only work on Windows. This is an engine limitation.
- Hiding some entities might not work, e.g. prop_glowing_object in L4D1.
- Two examples, for props and particles:
Models:
PHP Code:
bool g_bShowProp[MAXPLAYERS+1];
Action CmdProp(int client, int args)
{
// Let's say we created a prop but only want to display to certain players
int entity = CreateEntityByName("prop_dynamic");
// Other entity stuff here. SetEntityModel() etc. Ignored for example.
// Now we hook it. The Hook_SetTransmit callback is called every frame so it's best to use simple calculations inside.
SDKHook(entity, SDKHook_SetTransmit, Hook_SetTransmit);
// There are different ways to do this, suggest viewing other plugins for examples.
// We're going to store a value here to determine if the client can see the prop or not.
g_bShowProp[client] = true;
}
Action Hook_SetTransmit(int entity, int client)
{
// Check if the client is allowed to see the prop:
if( g_bShowProp[client] == true )
return Plugin_Continue; // This allows the prop to be visible for the client.
return Plugin_Handled; // This will hide it from the client if they're not allowed to see it.
}
Particles:
PHP Code:
bool g_bShowProp[MAXPLAYERS+1];
Action CmdProp(int client, int args)
{
// Let's say we created a particle but only want to display to certain players
int entity = CreateEntityByName("info_particle_system");
// Other entity stuff here. DispatchSpawn() etc. Ignored for example.
SDKHook(entity, SDKHook_SetTransmit, Hook_SetTransmit);
g_bShowProp[client] = true;
}
Action Hook_SetTransmit(int entity, int client)
{
// Check if we have allowed this client to see the prop
if( g_bShowProp[client] == true )
return Plugin_Continue; // This allows the prop to be visible for the client.
// The following two lines are used to hide the particles
// This might only work for some games or windows servers only.
if( GetEdictFlags(entity) & FL_EDICT_ALWAYS )
SetEdictFlags(entity, GetEdictFlags(entity) &~ FL_EDICT_ALWAYS);
return Plugin_Handled;
}
RequestFrame:
Spoiler
- CreateTimer() has a minimum value of 0.1 seconds. For anything quicker (e.g. the next game frame) it's better to use RequestFrame().
- When detecting projectiles in OnEntityCreated() and using SDKHook_SpawnPost() the velocity data is still not initialized, this is where I use RequestFrame() to get valid data.
PHP Code:
// Listen for thrown grenades.
public void OnEntityCreated(int entity, const char[] classname)
{
if( strcmp(classname, "molotov_projectile") == 0 )
{
// Wait for entity to have fully spawned
SDKHook(entity, SDKHook_SpawnPost, SpawnPost);
}
}
void SpawnPost(int entity)
{
// 1 frame later required to get velocity
RequestFrame(OnNextFrame, EntIndexToEntRef(entity));
}
void OnNextFrame(int entity)
{
// Validate entity
if( EntRefToEntIndex(entity) == INVALID_ENT_REFERENCE || !IsValidEntity(entity) )
return;
// Now the velocity is valid and not 0,0,0
float vPos[3];
GetEntPropVector(entity, Prop_Send, "m_vInitialVelocity", vPos);
}
- Example thanks to "Maxximou5" and "Desktop"
- Moving players to Spectator team after connection, this prevents any delay that might have spawned them somewhere first:
PHP Code:
#include <cstrike>
public void OnPluginStart()
{
HookEvent("player_connect_full", Event_PlayerConnectFull);
}
void Event_PlayerConnectFull(Event event, const char[] name, bool dontBroadcast)
{
int client = GetClientOfUserId(event.GetInt("userid"));
RequestFrame(Frame_ChangeTeam, client);
}
void Frame_ChangeTeam(any client)
{
ChangeClientTeam(client, CS_TEAM_SPECTATOR);
}
Various:
- Color includes: Colors.inc, stamm-colors.inc, morecolors.inc, multicolors.inc, colorvariables.inc, colorsLib.inc
- SteamID type: Steam2, Steam3 & Steam64. Use GetClientAuthId to retrieve the type you want. Good for storing in a file/database to persist after restarts.
- KeyValue configs - They are essential for a lot of stuff. Data configs, reading and saving. For examples: KeyValues SourceMod Scripting.
- SMCParser - Like KeyValue configs, but does not require knowing the keynames. For examples see SM scripting/admin-flatfile folders scripts. Or my VScript Replacer, Info Editor or Neon Beams for a very simple version.
- Client Preferences - Cookies are used to store client data. [Tutorial] ClientPrefs. Storage for simple per client info/settings.
- GCFScape - To open VPKs and get files like "modevents.res" as a source of events the game is using (other sources are mostly outdated). Also to open .BSP and extract map resources.
- VProf - To check the performance of plugins, you should only use in a testing environment and not live servers as this may be too intensive on the server.
- SM profiler API - To run benchmark testing on specific areas in your plugin, or check how fast executing certain bits of code are.
- Timers - Can be used to delay tasks e.g. welcome messages, or repeat something several times at specific intervals. View post #33 for multiple examples on correct usage.
- Translations - Used for Multi-Lingual support when displaying messages or translations in menus etc.
Information:
- An edict can only have an index between 0 and 2048. Above 2048 are non-networked entities that only the server can see (for example the logic_case entity).
- Newer games have higher limits.
- CS:GO can use up to 4096 edicts (I assume 4097 - 8192 are non-networked entities).
- Garrys Mod apparently supports up to 8192 edicts.
- IsValidEdict() will return false on non-networked entities. Use IsValidEntity() for them.
- FindEntityByClassname() will return an entity reference for non-networked entities (e.g. the entity number will be something like -2036529131).
- You can convert these with EntRefToEntIndex() to get the entity index value between 2049-4096, however unless you're using that for an array index it's best to stick with the reference.
- Format - Use when formatting a string where the destination buffer is also one of the formatting parameters.
PHP Code:
Format(buffer, sizeof(buffer), "%i %s", someInt, buffer);
- FormatEx - Use when formatting and destination buffer is NOT one of the formatting parameters (faster than Format).
PHP Code:
FormatEx(buffer, sizeof(buffer), "%s", someStr);
- strcopy - Use when just copying a string and not using formatting rules.
PHP Code:
strcopy(buffer, sizeof(buffer), "Some string");
strcopy(buffer, sizeof(buffer), someStr);
- StrCat - Use when adding onto the end of a string and not using formatting rules.
PHP Code:
StrCat(buffer, sizeof(buffer), "this text is appended to buffer");
- cl_showpos 1 client console cvars - to open mini-panel tracking your current position, angles, velocity.
- net_graph client console cvars - allows you to see fps, ping, lerp and network usage graph etc. More info.
- net_graph 4 is recommended.
- Here is a keybind example I use, when pressing Tab to view the scoreboard it will also show the net_graph:
Spoiler
PHP Code:
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
- Client cvars to simulate lag (can be useful for testing). These require sv_cheats on:
- net_fakejitter - Jitter fakelag packet time
- net_fakelag - Lag all incoming network data (including loopback) by this many milliseconds.
- net_fakeloss - Simulate packet loss as a percentage (negative means drop 1/n packets)
- net_droppackets - Drops next n packets on client
- net_chokeloop - Apply bandwidth choke to loopback packets
- sv_allow_wait_command - Server cvar to allow or disallow the wait command on clients connected to the server.
- This will outright crash clients if they used a wait cmd once and the server turns it off without clients restarting their game.
8. Advanced Techniques:
Object-Oriented API
MethodMaps
Enum Structs
Natives and Forwards
- Creating Natives - Basically a function your plugin creates that other plugins can call.
- Creating Forwards - Basically an event your plugin creates that notifies other plugins when something happens.
PreProcessor Compiler Directives
- Russian link - Information can be gleaned from this or translate for more details.
Trace-Rays
VTables - Used for SDKCalls, Detours or memory patches.
GameData Signatures - Used for SDKCalls, Detours or memory patches.
Memory Patching
SDKCalls
SDKHooks (Extension now part of SourceMod):
- SDKHooks Extension - Many useful functions and forwards, e.g. hooking OnTakeDamage, entity PreThink and OnEntityCreated/Destroyed forwards etc.
- Updated list and callback prototypes are always available in the include file.
Detouring Functions (Extension now part of SourceMod):
- DHooks Extension - Create custom hooks of game functions to either block, read or modify them. Useful for custom events or forwards too.
9. Good Coding Practices:
- Use RemoveEntity(entity); (in SM 1.10+) which is a faster and safer version of AcceptEntityInput(entity, "Kill"); (in SM <= 1.9) instead of RemoveEdict(entity).
- RemoveEdict in some cases can be safe and good for creating + deleting an entity in a single function when you only briefly need it, e.g. point_hurt and env_shake.
- RemoveEdict is known to crash the server, when used with certain entities or at the wrong time, e.g. when deleting a weapon that's equipped on a player.
- Before using or killing an entity, ensure it is valid with IsValidEntity();
- Necessarily check for zero if(entity != 0) or just if(entity), because 0 (on dedicated server) is a world and valid, so you can crash server instantly.
- No sense in using if IsClientConnected(client) && IsClientInGame(client) together since IsClientInGame already includes IsClientConnected check.
- Use uniform style for naming the variables. Good start is "Hungarian notation"
- e.g. prepend g_ if variable is declared in global scope.
- Use TIMER_FLAG_NO_MAPCHANGE to close timer handles automatically when the map ends. Does not close on round_end event only map change (OnMapEnd).
- It prevents from unexpected consequences.
- Unless you are storing the handle and manually killing the timer, in which case you would receive "Exception reported: Invalid timer handle".
- !!iValue - to convert "int" to "bool"
- I personally declare "char" variables as "static char" if they are large and called multiple times in expensive loops or Think* functions.
- This will optimize speed, because the variable is not allocated multiple times. BUT: This memory is never destroyed and always there.
- Only use if you understand what you're doing. This is only really beneficial for expensive functions and high tick servers with 50-100+ crazy plugins where optimizing becomes necessary.
- However: "using static for local arrays isn't good advice, it leads to many bugs and stack allocation is cheap." - asherkin.
- If you want assign the same value to all array items you can do it during initializing rather then in a loop later: int test[32] = {1, ...};
- Otherwise int test[32] fills the array with the default value, 0 in this case.
- TeleportEntity before DispatchSpawn. Doing the other way round can cause the server to crash. Happens with some models/entity types spawning at 0,0,0.
- switch instead of multiple if else statements. This is probably faster too. More info here.
- case section uses constant values. It can also support multiple values e.g. case 1,2:
- c[0] == 'a' instead of full string comparisons, e.g. when checking a list of known player models. Find an index in the string where each character is unique. Can also use switch().
- Good example by Lux can be found here.
- strcmp instead of StrEqual. The latter is simply a wrapper to the former and slower by creating an extra call.
- FormatEx instead of Format. When input and output buffers are NOT the same.
- Doing FormatEx(blah, sizeof(blah), "%s", someStr); is only very very slightly quicker than IntToString(i, blah, sizeof(blah)); but basically no comparison so use any.
- Both however are 3x to 10x quicker than doing Format(blah, sizeof(blah), "%d", i);
- StringMap.GetValue is slightly quicker than ArrayList.FindString and probably significantly faster for larger arrays.
- Preference SDKHook_Think and similar functions over using OnGameFrame.
- Create one timer for all players instead of separate timer for each one (example: HP regen plugins).
- delete and null are preferred over CloseHandle and INVALID_HANDLE. delete also sets the handle to null so you don't have to.
- Storing cvar handles, using HookConVarChange and storing the returned cvar values in global variables is more efficient than constantly using GetConvar* calls.
- See any number of my plugins for examples.
- Use %N instead of GetClientName() where possible, e.g. when using Format.
- You only need to put the keyword public before forwards from SourceMod and forwards from 3rd party plugins. Callbacks that you name yourself don't need "public" in front of them, for example CreateTimer, HookEvent, RequestFrame and other manually named callbacks.
- Late Loading - More info here.
- Late loading means a plugin was probably manually loaded during gameplay, or possibly reloaded.
- Because of this the plugin may not work as expected due to events not having fired.
- You can detect when a plugin is late loaded and setup everything to run normally.
Spoiler
PHP Code:
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) )
{
// Do something specific to in-game clients, as if they had connected after the plugin was loaded.
OnClientPutInServer(i);
}
}
}
}
public void OnClientPutInServer(int client)
{
// Stuff
}
- Unloading Plugins
- I advise to support unloading also, this means deleting entities your plugin created or restoring memory patches etc.
- Especially helpful when scripting and testing your plugin if you're constantly reloading the plugin, e.g. with Plugin AutoReload.
- This also prevents stuff like attached models getting stuck on players because they're no longer tracked and deleted.
- Supporting Listen Servers - Listen Servers are servers being hosted by a games client.
- Dedicated Servers are ALWAYS recommended over Listen servers.
- The index 0 on a Dedicated Server is the "World" entity. On Listen servers a clients index via console is also 0, which causes problems with commands used in the console.
- Executing commands from the console that expect a client to execute them will often cause errors.
- Some SourceMod functions do not support listen servers and expect the client index to be > 0. Another reason to use Dedicated Servers.
- Some features from SourceMod will also not work on Listen servers, for example SendConVarValue when I was testing.
- Simply put: Don't use Listen servers!
10. Common Scripting Mistakes:
Writing Sane Plugins - Many tips and common mistakes.
XY Problem - Need help? When asking questions, make sure you explain the real problem and not what you think might help fix it.
- Unable to load plugin (no debug string table) - you need to compile the plugin with an older version of SM, e.g. 1.9 (or better, update your servers SourceMod version).
- Illegal disk size - smx file is corrupted during ftp uploading. Re-upload it again.
- Client X is not connected or Client X is not in game - missing IsClientInGame() check in source code.
- Possibly lack of safely storing client indexes. See "Storing client and entity indexes" below.
- Invalid entity or Invalid edict - missing IsValidEntity() or IsValidEdict() check in source code.
- Possibly lack of safely storing entity indexes. See "Storing client and entity indexes" below.
- "Native XXX is not bound" - plugin dependency is not installed, not loaded yet or failed.
- If dependency is optional, always check if the native is available using GetFeatureStatus() in OnAllPluginsLoaded before calling it.
- Or check for library presence with LibraryExists() in OnAllPluginsLoaded if the dependency plugin registers one.
- "Instruction contained invalid parameter" - source code is successfully compiled despite syntax or structure error since parser is not ideal, example: un-initialized variable.
- In this case, error log can point to a wrong plugin name, e.g. normal plugin that leads to raising event in other - guilty plugin.
- Static variables - Know when to use, for retaining data in future function calls. Don't expect the variable to be initialized with it's default value on subsequent calls, e.g. don't expect strings to be empty.
- It's not recommended for local arrays. Usage example: when you don't need a global variable and a static bool can determine if you hooked something in a function so it can toggle or be called multiple times without causing issues.
- If for example you're calling the function multiple times trying to hook something and only a static bool saves it from multiple duplicate hooks, you might have to rethink your scripts logic.
- Compiling "error 075: input line too long (after substitutions) - File probably has the wrong encoding. Change to UTF-8.
- Alternatively duplicate a working script, replace with your code, delete the problem file and rename the duplicate to your filename.
- If you use Database transactions ensure you created the table as Transaction-Safe type, like "InnoDB": Tutorial.
- Don't use FakeClientCommand to call your plugins own registered command. Use the callback name like a function call e.g. CmdTest(client, 0);
- Some people think using OnPlayerRunCmd uses more CPU cycles than OnGameFrame, it does not. Also use a proper validation order, see below for more info.
- Wrong validation checks order. Doing bitwise math or checking against GetGameTime uses way less CPU cycles than native calls such as IsValidEntity or IsClientInGame. Do the former first before proceeding to more costly calls, especially vital in Think hooks or OnGameFrame functions.
- Wrong client validation order. The correct order would be: if (client && client <= MaxClients) to validate not 0 (doesn't support listen servers) and client index, IsClientConnected (mostly not required because the next call covers it), IsClientInGame, then your stuff e.g. GetClientTeam, IsPlayerAlive etc.
- Calling FindConVar multiple times for the same convar. Instead, only use in OnPluginStart and store the returned handle for later use.
- Use GetEngineVersion instead of GetGameFolderName. The latter is from older scripts before the former function was added.
- Using strlen to check if a string is empty. It's much quicker to check the first character: blah[0] == 0 or blah[0] == '\0' or !blah[0]
- Only using the round_end event to reset variables etc, you must consider people changing map half way through so the event never fires. Also reset in OnMapEnd or round_start event.
- You need to check the map has started before creating an entity.
- Before searching and modifying entities in OnMapStart(), you have to wait a bit to allow entities correctly initialize. It's better to use "round_freeze_end" event (not available for some games). Or try using RequestFrame.
- Local variable "v" shadows a variable at a preceding level - means you declared variable "v" twice.
- Sending too many UserMessages or a panel (SendPanelToClient) too often in a short period of time can crash clients with the error "Reliable buffer overflow".
- Menus and panels or their entries not appearing when "[" is the first character of the string. Instead use " [". Note this may affect listing client names that start with "[".
- Do: ReplaceString(ClientUserName, sizeof(ClientUserName)), "[", ""); OR Format(text_client, sizeof(text_client)), " %s", ClientUserName);
Unknown Command: - Missing return Plugin_Handled;
Spoiler
- When creating a command, you'll receive "Unknown command" in the console unless you end the callback with: return Plugin_Handled;
PHP Code:
public void OnPluginStart()
{
RegConsoleCmd("sm_test", CmdTest);
}
Action CmdTest(int client, int args)
{
// Stuff
return Plugin_Handled;
}
Storing client and entity indexes or passing them to Timers:
Spoiler
- To prevent affecting the wrong client if they have disconnected or the incorrect entity when it's been deleted we use the following commands to get a unique ID for them:
- GetClientUserId to store a clients userid (each new client connecting has a userid + 1 to the last person who joined).
- GetClientOfUserId to retrieve a clients index from userid, if they have disconnected the value will be 0. Must use with IsClientInGame too.
- EntIndexToEntRef to store an entity index by converting it into a unique serial reference.
- EntRefToEntIndex to retrieve an entity index from a reference, if the entity no longer exists the value will be -1 (INVALID_ENT_REFERENCE).
- Alternatives to GetClientUserId and GetClientOfUserId are: GetClientSerial and GetClientFromSerial.
- Here is an example of storing, retrieving and verifying client and entity indexes.
Entities:
PHP Code:
public void OnEntityCreated(int entity, const char[] classname)
{
// An entity spawned, it's entity index is between 0 and 2048 or 4096 for non-networked entities.
if( strcmp(classname "molotov_projectile") == 0 )
{
// Retrieve the reference to avoid affecting an entity that might have re-used the same entity index if ours has been deleted.
CreateTimer(5.0, TimerDelete, EntIndexToEntRef(entity));
}
}
Action TimerDelete(Handle timer, any entity)
{
// Convert the reference back into an entity index
entity = EntRefToEntIndex(entity);
// Check the entity value is not -1 and valid to be used
if( entity != INVALID_ENT_REFERENCE )
{
RemoveEntity(entity); // Safely kill this entity knowing it's exactly the original and intended entity.
} else {
// The entity no longer exists and we're safe by not mistakenly affecting another entity that has reused the same index.
}
}
Clients:
PHP Code:
int g_iClientList[MAXPLAYERS+1];
public void OnClientPutInServer(int client)
{
// Always pass a UserID into timers and validate in a timers callback
CreateTimer(10.0, TimerMessage, GetClientUserId(client));
// Another example: You might store the clients UserID for use in another function and want to make sure you're affecting the same person.
g_iClientList[client] = GetClientUserId(client);
}
Action TimerMessage(Handle timer, any client)
{
// Retrieve a clients index (should be in the range of 1 to MaxClients on a dedicated server)
client = GetClientOfUserId(client);
if( client != 0 && IsClientInGame(client) )
{
// This is the same client we wanted to affect.
} else {
// They must have disconnected.
}
}
// Another example e.g. calling a function that uses a stored list of client UserIDs:
void SomeFunction()
{
int client = GetClientOfUserId(g_iClientList[client]);
if( client && IsClientInGame(client) )
{
// This is the same person as earlier
} else {
// They must have disconnected.
}
}
OnEntityCreated - Retrieving Data:
Spoiler
- Trying to get the entity model in OnEntityCreated might not work, the proper way is using SDKHooks SpawnPost or RequestFrame.
- Some entities will not have valid position or velocity data yet, you must also use SpawnPost and possibly RequestFrame after that.
PHP Code:
public void OnEntityCreated(int entity, const char[] classname)
{
if( strcmp(classname "molotov_projectile") == 0 )
{
SDKHook(entity, SDKHook_SpawnPost, SpawnPost);
}
}
void SpawnPost(int entity)
{
// Validate
if( !IsValidEntity(entity) ) return;
// Model valid now
char sModel[64];
GetEntPropString(entity, Prop_Data, "m_ModelName", sModel, sizeof(sModel)));
// Depending on entity, velocity might not be valid until the next frame
RequestFrame(nextFrame, EntIndexToEntRef(entity)); // The 2nd argument to pass variables into the callback is optional, in this example we're using it.
}
void nextFrame(int entity)
{
// Validate
if( (entity = EntRefToEntIndex(entity)) != INVALID_ENT_REFERENCE )
{
// Now velocity data is correct
float vVel[3];
GetEntPropVector(entity, Prop_Send, "m_vInitialVelocity", vVel);
}
}
Declaring variables:
Spoiler
- Declare variables outside of loops. Putting them inside is very inefficient especially for strings. For example:
PHP Code:
// This is correct:
char temp[64];
int test;
for( int i = 1; i < 2048; i++ )
{
// Blah
}
// This is bad:
for( int i = 1; i < 2048; i++ )
{
char temp[64];
int test;
// Blah
}
Mixing variable types (int vs float):
Spoiler
PHP Code:
// Wrong:
float f = 1.5;
int i = 2;
i += f;
PrintToServer("wrong: %i", i); // Answer: 1080033280
// Correct is to cast float to int first using rounding:
float f = 1.5;
int i = 2;
i += RoundToCeil(f); // or RoundToFloor, RoundToZero, RoundFloat, RoundToNearest
PrintToServer("correct: %i", i); // Answer: 4
11. Debugging Plugins:Info by Dragokas: - Check addons/sourcemod/logs/error_<date>.log constantly; the most important parts are - error description (often omitted by users when they ask for help) and line number.
- Most direct and fastest method for resolving plugin conflicts or finding problem plugins is by - halving, by removing half of the plugins -> test -> repeat again.
- Install Accelerator Extension (works best on Linux) which provides various debugging information when a server crashes.
- Use -debug switch in servers startup command line parameters to get more detailed description in crash logs.
- Memory leaks can be found by using sm_dump_handles handles.txt and studying the result. Also good consolidator: https://hexer10.github.io/Sourcemod-HandleDumpParser/
Pay attention to entries with too many handles, like > 50 (except forwards), instead of total memory they used like some people think, unless you are the author to be able make such a conclusion.
- In that case it's better to compare memory by timeline to understand is it a leak:
Little explanation - make dump #1, wait till map end, make dump #2, change map, make dump #3; restart server, make dump #4, compare most suspicious values between dumps to see how memory/handle count increasing by time. In most cases, they should not, or should in a very limited range.
- Use ac_debug plugin (see attached in links post) to measure performance of each plugin using valve profiler (not working in L4D1/L4D2 at the moment).
- LogStackTrace function - Useful to write the last chain of function calls to error log. This is a feature from SM 1.10.
- Benchmarking plugin example:
- I will also run sm_test multiple times to get more of an average. Sometimes the minimum and maximum values will change.
Spoiler
PHP Code:
#include <profiler>
public void OnPluginStart()
{
RegAdminCmd("sm_test", sm_test, ADMFLAG_ROOT);
}
Action sm_test(int client, int args)
{
Handle hProf = CreateProfiler();
float min = 10.0, max, avg;
// Running the test 20 times to gain min/max/avg
for( int x = 0; x < 20; x++ )
{
StartProfiling(hProf);
// Running 1000 iterations for our test
for( int i = 0; i < 1000; i++ )
{
// Put the code or function you want to test here.
}
StopProfiling(hProf);
float speed = GetProfilerTime(hProf);
if( speed < min ) min = speed;
if( speed > max ) max = speed;
avg += speed;
}
// Display our minimum, maximum and average times.
avg /= 20;
PrintToServer("Bench: Min %f. Avg %f. Max %f", min, avg, max);
delete hProf;
return Plugin_Handled;
}
12. Compiling Plugins:
13. Releasing Plugins:
Recommendations when posting a new plugin thread.
Writing Sane Plugins - Many tips and common mistakes.
You'd be surprised how often people ask for basic information when it's not provided even if it seems obvious to you.
1. Provide an informative description about the plugins features and how to use them.
2. List all commands, cvars and the cvar config name and location.
3. Explain how to install the plugin, including paths to additional configs, models, sounds etc.
4. List and link to any requirements such as includes, plugins or extensions.
5. Only upload a pre-compiled .SMX if you're using custom includes that prevent it from compiling on the forum using the "Get Plugin" link.
6. Use underscores "_" in filenames. Spaces " " prevent manually loading plugins and downloading the plugin they appear with "%20" in the name.
7. Also don't use characters such as "[" or "]" or "&" in filenames as these also show up as "%5B", "%5D" and "%26" etc when downloading from AlliedMods.
Credits:
- SourceMod and MetaMod developers for their great work.
- SourceMod wiki contributors, whose many articles are linked to.
- The plugin authors and SourceMod community for various plugins and links in this article.
- Dragokas for tons of additions to this thread and many recommendations!
- Maxximou5, Marttt, zipcore, Desktop, MAGNAT2645, xZk, asherkin, JoinedSenses, Lux, GAMMACASE and SM9(); for their contributions to this thread.
__________________
Last edited by Silvers; 11-18-2022 at 06:39.
|
|