Difference between revisions of "Writing Multi-Game SourceMod Plugins"

From AlliedModders Wiki
Jump to: navigation, search
(More headshot stuff)
m (Update highlighting)
 
(9 intermediate revisions by 3 users not shown)
Line 7: Line 7:
 
To detect the current game, use the GetEngineVersion() function, like this:
 
To detect the current game, use the GetEngineVersion() function, like this:
  
<pawn>new EngineVersion:g_EngineVersion;
+
<sourcepawn>
 +
EngineVersion g_EngineVersion;
  
// In OnPluginStart
+
public void OnPluginStart()
 +
{
 
     g_EngineVersion = GetEngineVersion();
 
     g_EngineVersion = GetEngineVersion();
</pawn>
+
}
 +
</sourcepawn>
  
 
This isn't always necessary for games that use different events, but it's still a good idea.
 
This isn't always necessary for games that use different events, but it's still a good idea.
Line 41: Line 44:
 
| No
 
| No
 
| [[Team Fortress 2 Events#teamplay_round_start|teamplay_round_start]]
 
| [[Team Fortress 2 Events#teamplay_round_start|teamplay_round_start]]
| '''full_round''' will be false if this is not the first round of a multi-round map. In Arena mode, the [[https://wiki.alliedmods.net/Team Fortress 2 Events#arena_round_start|arena_round_start]] event fires when players can start moving.
+
| '''full_reset''' will be false if this is not the first round of a multi-round map. In Arena mode, the [[Team Fortress 2 Events#arena_round_start|arena_round_start]] event fires when players can start moving.
 
|-
 
|-
 
| Counter-Strike: Global Offensive
 
| Counter-Strike: Global Offensive
Line 119: Line 122:
 
Team Fortress 2 introduced the concept of fake deaths.  In order to detect if a death was fake, you need to do something like this:
 
Team Fortress 2 introduced the concept of fake deaths.  In order to detect if a death was fake, you need to do something like this:
  
<pawn>
+
<sourcepawn>
// At top of file
 
 
#include <tf2_stocks>
 
#include <tf2_stocks>
  
// in player_death hook
+
// Hook the "player_death" event during OnPluginStart and pass in the following callback:
  new bool:bFake = g_EngineVersion == Engine_TF2 && GetEventInt(event, "death_flags") & TF_DEATHFLAG_DEADRINGER;
+
public Action Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
  // if bFake is true, the it was a fake death
+
{
</pawn>
+
    bool bFake = g_EngineVersion == Engine_TF2 && (event.GetInt("death_flags") & TF_DEATHFLAG_DEADRINGER);
 +
    if (bFake)
 +
    {
 +
        // Do stuff on faked death
 +
    }
 +
    // do other stuff
 +
}
 +
</sourcepawn>
  
=== Detecting Headshots ===
+
== Detecting Headshots ==
  
 
Not all games support headshots, but those that do have them done differently.
 
Not all games support headshots, but those that do have them done differently.
  
  
==== player_death ====
+
=== player_death ===
  
 
Headshots can be detected in player_death similarly to this:
 
Headshots can be detected in player_death similarly to this:
  
<pawn>
+
<sourcepawn>
     new bool:bHeadshot;
+
#include <tf2_stocks>
 +
 
 +
// Hook the "player_death" event during OnPluginStart and pass in the following callback:
 +
public Action Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
 +
{
 +
     bool bHeadshot;
  
 
     switch (g_EngineVersion)
 
     switch (g_EngineVersion)
 
     {
 
     {
         case Engine_CSS, Engine_CSGO, Engine_L4D, Engine_L4D2:
+
         case Engine_CSS, Engine_CSGO, Engine_Left4Dead, Engine_Left4Dead2:
 
         {
 
         {
             bHeadshot = GetEventBool(event, "headshot");
+
             bHeadshot = event.GetBool("headshot");
 
         }
 
         }
  
 
         case Engine_TF2:
 
         case Engine_TF2:
 
         {
 
         {
             bHeadshot = GetEventInt(event, "customkill") == TF_CUSTOM_HEADSHOT;
+
             bHeadshot = event.GetInt("customkill") == TF_CUSTOM_HEADSHOT;
 
         }
 
         }
 
     }
 
     }
Line 157: Line 171:
 
         // This was a headshot, do something
 
         // This was a headshot, do something
 
     }
 
     }
</pawn>
+
}
 +
</sourcepawn>
  
 
Unfortunately, some games don't appear to track headshots for death (Half-Life 2: DeathMatch for example).
 
Unfortunately, some games don't appear to track headshots for death (Half-Life 2: DeathMatch for example).
  
==== player_hurt ====
+
=== player_hurt ===
 
player_hurt is a bit trickier.
 
player_hurt is a bit trickier.
  
<pawn>
+
<sourcepawn>
 +
#include <tf2_stocks>
  
// At top of file
 
 
#define HITGROUP_HEAD 1
 
#define HITGROUP_HEAD 1
  
// in player_hurt hook
+
// Hook "player_hurt", etc.
 
+
public Action Event_PlayerHurt(Event event, const char[] name, bool dontBroadcast)
     new bool:bHeadshot;
+
{
 +
     bool bHeadshot;
  
 
     switch (g_EngineVersion)
 
     switch (g_EngineVersion)
 
     {
 
     {
         case Engine_CSS, Engine_CSGO, Engine_DODS, Engine_L4D, Engine_L4D2:
+
         case Engine_CSS, Engine_CSGO, Engine_DODS, Engine_Left4Dead, Engine_Left4Dead2:
 
         {
 
         {
             bHeadshot = GetEventInt(event, "hitgroup") == HITGROUP_HEAD;
+
             bHeadshot = event.GetInt("hitgroup") == HITGROUP_HEAD;
 
         }
 
         }
  
 
         case Engine_TF2:
 
         case Engine_TF2:
 
         {
 
         {
             bHeadshot = GetEventInt(event, "custom") == TF_CUSTOM_HEADSHOT;
+
             bHeadshot = event.GetInt("custom") == TF_CUSTOM_HEADSHOT;
 
         }
 
         }
 
     }
 
     }
Line 190: Line 206:
 
         // This was a headshot, do something
 
         // This was a headshot, do something
 
     }
 
     }
</pawn>
+
}
 +
</sourcepawn>
  
 
As you can see, more games support detecting if a shot was a headshot in player_hurt than player_death.
 
As you can see, more games support detecting if a shot was a headshot in player_hurt than player_death.
  
==== SDKHooks TraceAttack and OnTakeDamage ====
+
=== SDKHooks TraceAttack and OnTakeDamage ===
  
 
Headshots can be detected by TraceAttack or OnTakeDamage depending on the game.
 
Headshots can be detected by TraceAttack or OnTakeDamage depending on the game.
Line 200: Line 217:
 
Note: Changing damage and returning Plugin_Changed for either event will change the damage the player takes.
 
Note: Changing damage and returning Plugin_Changed for either event will change the damage the player takes.
  
<pawn>
+
<sourcepawn>
 +
#include <sdkhooks>
 +
 
 +
#define HITGROUP_HEAD 1
  
public OnClientPutInServer(client)
+
public void OnClientPutInServer(int client)
 
{
 
{
 
     switch (g_EngineVersion)
 
     switch (g_EngineVersion)
 
     {
 
     {
         case Engine_CSS, Engine_CSGO, Engine_DODS, Engine_L4D, Engine_L4D2:
+
         case Engine_CSS, Engine_CSGO, Engine_DODS, Engine_Left4Dead, Engine_Left4Dead2:
 
         {
 
         {
 
             SDKHook(client, SDKHook_TraceAttack, TraceAttack);
 
             SDKHook(client, SDKHook_TraceAttack, TraceAttack);
 
         }
 
         }
 
 
         case Engine_TF2:
 
         case Engine_TF2:
 
         {
 
         {
             SDKHook(client, SDKHook_TraceAttack, OnTakeDamage);
+
             SDKHook(client, SDKHook_OnTakeDamage, OnTakeDamage);
 
         }
 
         }
 
     }
 
     }
Line 219: Line 238:
 
}
 
}
  
public Action:TraceAttack(victim, &attacker, &inflictor, &Float:damage, &damagetype, &ammotype, hitbox, hitgroup)
+
public Action TraceAttack(int victim, int &attacker, int &inflictor, float &damage,  
 +
        int &damagetype, int &ammotype, int hitbox, int hitgroup)
 
{
 
{
     new bool:bHeadshot = hitgroup == HITGROUP_HEAD;
+
     bool bHeadshot = hitgroup == HITGROUP_HEAD;
  
 
     if (bHeadshot)
 
     if (bHeadshot)
Line 231: Line 251:
 
}
 
}
  
public Action:OnTakeDamage(victim, &attacker, &inflictor, &Float:damage, &damagetype, &weapon,
+
public Action OnTakeDamage(int victim, int &attacker, int &inflictor, float &damage,  
Float:damageForce[3], Float:damagePosition[3], damagecustom)
+
        int &damagetype, int &weapon, float damageForce[3], float damagePosition[3], int damagecustom)
 
+
{
     new bool:bHeadshot = damagecustom == TF_CUSTOM_HEADSHOT;
+
     bool bHeadshot = damagecustom == TF_CUSTOM_HEADSHOT;
 
      
 
      
 
     if (bHeadshot)
 
     if (bHeadshot)
Line 242: Line 262:
  
 
     return Plugin_Continue;
 
     return Plugin_Continue;
}</pawn>
+
}
 +
</sourcepawn>
 +
 
 +
[[Category:SourceMod Scripting]]

Latest revision as of 19:29, 29 March 2020

Different Source games tend to have different quirks. This page discusses the more common quirks you'll run into.

Detecting the current game

In order to use what you learn from this document, you will need to detect which game the plugin is running on.

To detect the current game, use the GetEngineVersion() function, like this:

EngineVersion g_EngineVersion;
 
public void OnPluginStart()
{
    g_EngineVersion = GetEngineVersion();
}

This isn't always necessary for games that use different events, but it's still a good idea.

Round Start

Most plugin writers assume that hooking round_start will make things work on all games. Those plugin writers would be wrong.

Several games use other events instead or have behavior that needs to be taken into account.

Game Fires round_start? Alternate Event Extra Quirks
Half-Life 2: DeathMatch Yes teamplay_round_start teamplay_round_start is only fired for Team Deathmatch
Day of Defeat: Source No dod_round_start
Team Fortress 2 No teamplay_round_start full_reset will be false if this is not the first round of a multi-round map. In Arena mode, the arena_round_start event fires when players can start moving.
Counter-Strike: Global Offensive Yes round_start is fired during warmup round

Round End

round_end, like round_start, isn't the same across all games.

Most of the time, winner is the team ID of the winning team: 0 for stalemate, 2 for team 1 (Combine, Allies, Terrorists, RED, or Survivors), or 3 for team 2 (Rebels, Axis, Counter-Terrorists, BLU, or Infected).

Game Fires round_end? Alternate Event Extra Quirks
Half-Life 2: DeathMatch Yes winner is a userid for Deathmatch or a team ID for Team Deathmatch
Day of Defeat: Source No dod_round_win team is the winning team.
Team Fortress 2 No See next section See next section
Nuclear Dawn No round_win team is the winning team. type is the win reason.

Team Fortress 2

Round End is so complicated in Team Fortress 2 that I'm giving it its own section. TF2 has not just one, but four different events for round end.

Game Mode Event Quirks
All teamplay_round_win team is the winning team. winreason was only recently fixed and its meaning depends on the game mode.
All but Arena or MvM teamplay_win_panel Win Panel screen, includes each team's score and the scores of the top 3 players. winning_team is the winning team. winreason includes captured all points, time ran out, captured flags, etc...
Arena arena_win_panel Arena Win Panel screen, includes each team's score and the scores of the top 3 players for each team. winning_team is the winning team. winreason can only be killed all players on the other team and captured point.
MvM pve_win_panel MvM Win Panel screen. winning_team is the winning team. Most data is not present in this event and is instead interpolated from the CTFPlayerResource netprops.

player_death

All games appear to use the player_death event, but the fields available to each game vary radically.

The quirks you need to know about are:

Detecting Fake Deaths

Team Fortress 2 introduced the concept of fake deaths. In order to detect if a death was fake, you need to do something like this:

#include <tf2_stocks>
 
// Hook the "player_death" event during OnPluginStart and pass in the following callback:
public Action Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
{
    bool bFake = g_EngineVersion == Engine_TF2 && (event.GetInt("death_flags") & TF_DEATHFLAG_DEADRINGER);
    if (bFake)
    {
        // Do stuff on faked death
    }
    // do other stuff
}

Detecting Headshots

Not all games support headshots, but those that do have them done differently.


player_death

Headshots can be detected in player_death similarly to this:

#include <tf2_stocks>
 
// Hook the "player_death" event during OnPluginStart and pass in the following callback:
public Action Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
{
    bool bHeadshot;
 
    switch (g_EngineVersion)
    {
        case Engine_CSS, Engine_CSGO, Engine_Left4Dead, Engine_Left4Dead2:
        {
            bHeadshot = event.GetBool("headshot");
        }
 
        case Engine_TF2:
        {
            bHeadshot = event.GetInt("customkill") == TF_CUSTOM_HEADSHOT;
        }
    }
 
    if (bHeadshot)
    {
        // This was a headshot, do something
    }
}

Unfortunately, some games don't appear to track headshots for death (Half-Life 2: DeathMatch for example).

player_hurt

player_hurt is a bit trickier.

#include <tf2_stocks>
 
#define HITGROUP_HEAD 1
 
// Hook "player_hurt", etc.
public Action Event_PlayerHurt(Event event, const char[] name, bool dontBroadcast)
{
    bool bHeadshot;
 
    switch (g_EngineVersion)
    {
        case Engine_CSS, Engine_CSGO, Engine_DODS, Engine_Left4Dead, Engine_Left4Dead2:
        {
            bHeadshot = event.GetInt("hitgroup") == HITGROUP_HEAD;
        }
 
        case Engine_TF2:
        {
            bHeadshot = event.GetInt("custom") == TF_CUSTOM_HEADSHOT;
        }
    }
 
    if (bHeadshot)
    {
        // This was a headshot, do something
    }
}

As you can see, more games support detecting if a shot was a headshot in player_hurt than player_death.

SDKHooks TraceAttack and OnTakeDamage

Headshots can be detected by TraceAttack or OnTakeDamage depending on the game.

Note: Changing damage and returning Plugin_Changed for either event will change the damage the player takes.

#include <sdkhooks>
 
#define HITGROUP_HEAD 1
 
public void OnClientPutInServer(int client)
{
    switch (g_EngineVersion)
    {
        case Engine_CSS, Engine_CSGO, Engine_DODS, Engine_Left4Dead, Engine_Left4Dead2:
        {
            SDKHook(client, SDKHook_TraceAttack, TraceAttack);
        }
        case Engine_TF2:
        {
            SDKHook(client, SDKHook_OnTakeDamage, OnTakeDamage);
        }
    }
 
}
 
public Action TraceAttack(int victim, int &attacker, int &inflictor, float &damage, 
        int &damagetype, int &ammotype, int hitbox, int hitgroup)
{
    bool bHeadshot = hitgroup == HITGROUP_HEAD;
 
    if (bHeadshot)
    {
        // This was a headshot, do something.
    }
 
    return Plugin_Continue;
}
 
public Action OnTakeDamage(int victim, int &attacker, int &inflictor, float &damage, 
        int &damagetype, int &weapon, float damageForce[3], float damagePosition[3], int damagecustom)
{
    bool bHeadshot = damagecustom == TF_CUSTOM_HEADSHOT;
 
    if (bHeadshot)
    {
        // This was a headshot, do something
    }
 
    return Plugin_Continue;
}