View Single Post
Author Message
Silvers
SourceMod Plugin Approver
Join Date: Aug 2010
Location: SpaceX
Old 01-24-2020 , 18:39   [TUT] SourcePawn Scripting - Tips, Basics to Advanced
Reply With Quote #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

Example Windows .bat file to start a CS:GO server which automatically starts on de_dust2 map.
Spoiler

Example Windows .bat file to start an L4D2 server and display a list of options to select which map the server starts on.
Spoiler




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




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


- Sublime Text




5. Useful Plugins and Extensions for Developers:




6. Server Fixes - (plugins + extensions for various games):
ANY:
Spoiler


CS:S:
Spoiler


CS:GO:
Spoiler


L4D1:
Spoiler


L4D2:
Spoiler





7. Various Functions and Information:

Hammer and Authoring Tools:
Spoiler


Particles:
Spoiler


AddFileToDownloadsTable: (custom content)
Spoiler


Entity Properties: (Prop_Data and Prop_Send)
Spoiler


Events:
Spoiler


Sound Hooks:
Spoiler


SetTransmit: (Hiding models from certain players):
Spoiler


RequestFrame:
Spoiler


Various:


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(buffersizeof(buffer), "%i %s"someIntbuffer); 
  • FormatEx - Use when formatting and destination buffer is NOT one of the formatting parameters (faster than Format).
    PHP Code:
    FormatEx(buffersizeof(buffer), "%s"someStr); 
  • strcopy - Use when just copying a string and not using formatting rules.
    PHP Code:
    strcopy(buffersizeof(buffer), "Some string");
    strcopy(buffersizeof(buffer), someStr); 
  • StrCat - Use when adding onto the end of a string and not using formatting rules.
    PHP Code:
    StrCat(buffersizeof(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

  • 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
  • ToDo.

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


  • 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


Storing client and entity indexes or passing them to Timers:
Spoiler


OnEntityCreated - Retrieving Data:
Spoiler


Declaring variables:
Spoiler


Mixing variable types (int vs float):
Spoiler




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




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.
Silvers is offline