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.
SourceMod Development - "This category contains articles about developing for SourceMod extensions" (Advanced)
Valve Wiki - Tons of great information from details on various entities to VScripts (CS:GO / L4D2).
Click the first link above and click on your specific game.
Third party tools - e.g. .VPK archive extractors, .nuc VScript decoders, convert .BSP maps into their source and open with Hammer Editor - useful for viewing how maps do certain things.
CS:GO and L4D2 may want to use the VScript File Replacer plugin to decode and dump VScripts.
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"
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"
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:
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
Sublime Text - Supports various programming languages, SourceMod community has made custom modifications for SourcePawn:
AutoKickAFK When ServerFull - by Lux
- Auto kick solution while server is full or just standard afk auto kicker
CS:S:
Spoiler
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:
Spoiler
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)
Movement Unlocker by Peace-Maker
- Removes max speed limitation from players on the ground. Feels like CS:S.
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.
For L4D/2 go into your Steam > Library > Tools and download the L4D/2 Authoring Tools, this includes a Model Viewer useful for identifying animation numbers and attachment points used for "SetParent" stuff. You may have to extract the .mdl models into your games /models/ folder to view.
CS:GO hammer is available by purchasing the Prime Status Upgrade from CS:GO's Steam Store page. Reference: here and here.
Stripper:Source extension - Instead of decompiling maps use this to dump a maps data and view how things are done.
- I have used this and hammer to replicate various features in plugins for L4D2.
- Stripper is also useful to modify the maps entities, see it's thread for more details.
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).
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;
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)
Spoiler
The AddFileToDownloadsTable function lets you send custom files to clients.
SourceMod provides several commands for dumping useful data. To see a full list type find sm_dump in your server console.
Enter the following commands into your servers console to generate and save the files to your games folder where you find addons, bin, cfg, maps folders.
NetProps and DataMaps are game variables used by entities to store data. NetProps are transmitted to clients while DataMaps are only accessible to the server.
Game Events (Source) - List of various events, generic and game specific. (Some data maybe missing or outdated).
You can use GCFScape to open your games .VPK files and extract various .res files by searching for "events" in the resource folder.
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);
}
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;
}
}
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.
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));
}
SteamID type: Steam2, Steam3 & Steam64. Use GetClientAuthId to retrieve the type you want. Good for storing in a file/database to persist after restarts.
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.
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.
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.
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.
Calling Functions - You can call game functions themselves to create unique things.
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!
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);
}
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).
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.
// 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;
Loading Plugins:
- How to load plugins without restarting the server.
- Can also use the command: sm plugins refresh
- DevCmds plugin provides: sm_refresh to refresh plugins and sm_renew to reload all.
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.
Replying is fine, I'll update the post every so often. I don't know how to use any Git stuff.
Is GetClientSerial better than GetClientUserID? Any significant differences? I use the latter simply because events also provide "userid" which I can use instead of converting keeping everything the same. The only problem with GetClientUserID is if you have (I think the value is) 65,536 connections it resets back to 1. But I don't think any servers are active for weeks or months to generate enough traffic. Also unless you're storing the UserID and checking against the list it shouldn't be a problem.
Thanks for the link will add that to the list too, good information there.
Thanks!!!
That's nice you consolidated useful links and lot of own practice in one place.
Tools:
Dunno, should it be mentioned. Everybody have own tastes.
I'll just leave here own toolset I often use:
-------------------------------
VPK:
- GCFScape - extracting VPK
- vpk.exe (included in game) - packing to VPK, just drag & drop folder on it
Source Code Helpers:
- Spider - web-based sp compiler with custom *.inc support
- WinMerge (freeware), Beyond Compare (trial) - compare differences between plugin versions
- Lysis - for decompilation - when e.g., you don't remember what plugin is installed on your server and can't find source or dunno what version is it.
- spcomp snapshots of other versions (1.11, 1.10, 1.9, 1.8, 1.7) - to compare 2 smx (last known source code and unknown smx), like such algorithm:
use decompiler to see in header the version of compiler unknown smx was compile with
use same compiler to create most similar smx from last known sp source code
decompile your own smx compiled in previous step
compare those pair in text mode
-------------------------------
// TODO for me.
Quote:
Originally Posted by Silvers
ConVars Anomaly Fixer
Perhaps need tutorial how effectively use it. Will do later.
Skip it. Already included in 1st post:
Spoiler
Quote:
Originally Posted by Silvers
Cvar Configs Updater - Good for updating convar configs when plugins update and add new convars
and to remove unused (to clean the server from garbage). And therefore decrease probability of cvar anomaly.
Common Scripting Mistakes:
You can append about opportunity to retrieve SteamId (GetClientAuthId, GetSteamAccountID) in case author wants to preserve user specific info in file/DB to survive the server restart.
Quote:
Originally Posted by Silvers
I'm unfamiliar with how to download Hammer for CS:GO etc and related tools (please let me know).
"bin" folder with hammer is only downloaded for users with Prime-status:
- By purchasing the Prime Status Upgrade from CS:GO's Steam Store page
- By reaching Profile Level 21 in CS:GO.
Particles:
You can append that you can access Particle Editor through "L4D Authoring Tools" -> L4D (Tools Mode).
Also, [Batch] Particles Extractor & Tester plugin (if suitable)
AddFileToDownloadsTable:
Possibly, need a link to some good article about all stuff regarding setup content-server, packing bz2, black screen and so.
Look also: https://forums.alliedmods.net/showthread.php?t=224404
Maybe, add to article. Though, looks pretty overloaded.
Debugging:
I think, need to add some basic info on debugging, like:
- where crash dumps are stored
- using -debug switch to get more detailed description in crash logs.
- install Accelerator
- check addons/sourcemod/logs/error_<date>.log constantly; hightlight that most important parts are - error description (often omitted by users when they asking for help) and line number.
- memory leak and how to dump handles, also provide link to this good consolidator: https://hexer10.github.io/Sourcemod-HandleDumpParser/
and explain to pay attention on records with too may handles, like > 50 (excepting forwards),
instead of total memory they used like some people think, unless you are the author to be able make such conclusion).
In that case it's better 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.
- explain the most direct and fast method of resolving plugin conflicts - halving, by removing half of plugins -> test -> repeat again.
Thanks Silvers, there is some great info in this thread.
As a suggestion:
*I would add as a "common mistake", trying to get the entity model on OnEntityCreated, the proper way to do it is getting it on SpawnPost or using NextFrame (this rule applies to other settings as well)
*I usually add in my scripts the #file tag to reduce the pathname of the file while compiling and fixing errors or warnings, this helps me to "read" better the log error/compiler output
*Would be good to add some link references (I don't know if there is any) on how to work properly with: SetTransmit / Client Cookies / Translations.
*Some SM online compilers available: SM Compiler Spider
@Dragokas: Thanks, added most to first post and linked your post.
@Marttt and @zipcore: Thanks for the additional information, some great points there, will update when I can.
Quote:
Originally Posted by Marttt
*I usually add in my scripts the #file tag to reduce the pathname of the file while compiling and fixing errors or warnings, this helps me to "read" better the log error/compiler output
The problem with this in NotePad++ (possibly other editors) when compiling the plugin and warnings/errors show up, you cannot double click the console output to jump to the line because the file doesn't exists, because it's been renamed. For this reason I don't recommend it.
Useful commands:
- report_entities - calculate identical entity classes on the map and report in server console, useful for debug too.
- cl_showents - report list of entities and its endeces (require sm_cvar sv_cheats 1)
// TODO:
Explain difference between:
- delete, CloseHande, methodmap.Close()
- IsValidEdict(), IsValidEntity()
- Prop_Send, Prop_Data and value limits for them (signed, unsigned)
Explain most common entity use stuff:
- move type and link to enum
- collion group, what is it and link to enum: https://developer.valvesoftware.com/...llision_groups
- ChangeEdictState use cases
- difference between client character, model, and class
- at least mention about "Navigation mode" that it is even exist.
> Really Soooorry about overloading you. Just a possible plan to improve the article. Not request.
Perhaps, something is more suitable for separate topic.
Skip it. Already included in 1st post:
Spoiler
// Todos: Explain difference between:
... Explain most common entity use stuff:
- do not declare variables as static when you pass them in recurse function (in most cases it is wrong and can cause crash).
SilverShot, I would suggest you to update installation and all subsequent batch scripts with the following syntax of paths:
Code:
@echo off
set "STEAM=D:\dev\SteamCMD"
set "GAME_DIR=D:\server\l4d2"
set "APP_ID=222860"
set "STEAM_USERNAME=anonymous"
set "STEAM_PASSWORD="
md "%GAME_DIR%"
cd /d "%STEAM%"
start "" steamcmd.exe +login "%STEAM_USERNAME%" "%STEAM_PASSWORD%" +force_install_dir "%GAME_DIR%" +app_update %APP_ID% validate
because when you copy code from forum it automatically insert invisible space at the end of each line causing SteamCMD can't connect due to incorrect login name.
I also added some corrections for reliability (like when script and folder is on different disks).
---
Perhaps, it's good to see listing of short solutions for most common errors in logs, like:
Quote:
* Unable to load plugin (no debug string table) - you need to compile plugin with older version of sm, e.g. 1.9 (or better, update SourceMod version of your server).
* 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.
Code practice:
- Use AcceptEntityInput(entity, "Kill") instead of RemoveEdict(entity). It is much safer.
(I saw it in 1st post, but I think, require hightlighting separately).
- Before using or killing entity, ensure it is valid with IsValidEntity(),
and 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.
- 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 handle of timer automatically when map is about to end. It prevent from unexpected consequences.
- !!iValue - to convert "int" to "bool" (somewhere was a better solution(?))
For advanced users (code optimizations):
- declare "char" type of variables as "static" if your function called several times. This will optimize speed, because variable is not allocated twice.
Mistakes (for advanced):
- if you use Database's transactions ensure you created table as Transaction-Safe type, like "InnoDB": https://www.tutorialspoint.com/mysql...ansactions.htm
(perhaps, need to add this notice directly in sm wiki)
Basics:
- floating point operations specific, like:
incorrect:
PHP Code:
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:
PHP Code:
float f = 1.5; int i = 2; i += RoundToCeil(f); // or RoundToFloor, RoundToZero, RoundFloat, RoundToNearest PrintToServer("correct: %i", i); // Answer: 4
For basics section (?), I think:
- list all possible abilities to store config externally (ConVars, Cookies, KeyValues, SMCParser, Database, simple file)
Useful commands:
- sm_dump_netprops_xml prop_netprops.xml - dump network properties in xml format
To Marttt:
Spoiler
Quote:
Originally Posted by Marttt
*I usually add in my scripts the #file tag to reduce the pathname of the file while compiling and fixing errors or warnings, this helps me to "read" better the log error/compiler output
I am also against this because:
- #file hides path in log and that full path is the only ability to find what version smx is compiled from for troubleshooting purposes since sm doesn't expose version number in error logs.
- with #file you receive a mess in log comes with different strings format of plugins who use #file and who doesn't.
- there are not so many lines in error logs, usually. Anyway, checkmark "Do not wrap lines" in text editor always solve such problem of convenient reading.