Difference between revisions of "CSGO Quirks"
(Antoher example of precache sound) |
(Added EmitSoundAny and wrappers) |
||
Line 58: | Line 58: | ||
Alternately, if you are going to have sounds in a plugin that supports multiple games, you can easily do it like this, noting that the * is missing from the relative sound path. | Alternately, if you are going to have sounds in a plugin that supports multiple games, you can easily do it like this, noting that the * is missing from the relative sound path. | ||
+ | |||
+ | Keep in mind that you need to play sounds with one of the EmitSound*Any stocks. | ||
<pawn> | <pawn> | ||
Line 113: | Line 115: | ||
return true; | return true; | ||
} | } | ||
− | </pawn> | + | |
+ | stock EmitSoundAny(const clients[], | ||
+ | numClients, | ||
+ | const String:sample[], | ||
+ | entity = SOUND_FROM_PLAYER, | ||
+ | channel = SNDCHAN_AUTO, | ||
+ | level = SNDLEVEL_NORMAL, | ||
+ | flags = SND_NOFLAGS, | ||
+ | Float:volume = SNDVOL_NORMAL, | ||
+ | pitch = SNDPITCH_NORMAL, | ||
+ | speakerentity = -1, | ||
+ | const Float:origin[3] = NULL_VECTOR, | ||
+ | const Float:dir[3] = NULL_VECTOR, | ||
+ | bool:updatePos = true, | ||
+ | Float:soundtime = 0.0) | ||
+ | { | ||
+ | decl String:szSound[PLATFORM_MAX_PATH]; | ||
+ | |||
+ | if (g_bNeedsFakePrecache) | ||
+ | { | ||
+ | Format(szSound, sizeof(szSound), "*%s", sample); | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | strcopy(szSound, sizeof(szSound), sample); | ||
+ | } | ||
+ | |||
+ | EmitSound(clients, numClients, szSound, entity, channel, level, flags, volume, pitch, speakerentity, origin, dir, updatePos, soundtime); | ||
+ | } | ||
+ | |||
+ | stock EmitSoundToClientAny(client, | ||
+ | const String:sample[], | ||
+ | entity = SOUND_FROM_PLAYER, | ||
+ | channel = SNDCHAN_AUTO, | ||
+ | level = SNDLEVEL_NORMAL, | ||
+ | flags = SND_NOFLAGS, | ||
+ | Float:volume = SNDVOL_NORMAL, | ||
+ | pitch = SNDPITCH_NORMAL, | ||
+ | speakerentity = -1, | ||
+ | const Float:origin[3] = NULL_VECTOR, | ||
+ | const Float:dir[3] = NULL_VECTOR, | ||
+ | bool:updatePos = true, | ||
+ | Float:soundtime = 0.0) | ||
+ | { | ||
+ | new clients[1]; | ||
+ | clients[0] = client; | ||
+ | /* Save some work for SDKTools and remove SOUND_FROM_PLAYER references */ | ||
+ | entity = (entity == SOUND_FROM_PLAYER) ? client : entity; | ||
+ | EmitSoundAny(clients, 1, sample, entity, channel, | ||
+ | level, flags, volume, pitch, speakerentity, | ||
+ | origin, dir, updatePos, soundtime); | ||
+ | } | ||
+ | |||
+ | stock EmitSoundToAllAny(const String:sample[], | ||
+ | entity = SOUND_FROM_PLAYER, | ||
+ | channel = SNDCHAN_AUTO, | ||
+ | level = SNDLEVEL_NORMAL, | ||
+ | flags = SND_NOFLAGS, | ||
+ | Float:volume = SNDVOL_NORMAL, | ||
+ | pitch = SNDPITCH_NORMAL, | ||
+ | speakerentity = -1, | ||
+ | const Float:origin[3] = NULL_VECTOR, | ||
+ | const Float:dir[3] = NULL_VECTOR, | ||
+ | bool:updatePos = true, | ||
+ | Float:soundtime = 0.0) | ||
+ | { | ||
+ | new clients[MaxClients]; | ||
+ | new total = 0; | ||
+ | |||
+ | for (new i=1; i<=MaxClients; i++) | ||
+ | { | ||
+ | if (IsClientInGame(i)) | ||
+ | { | ||
+ | clients[total++] = i; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | if (!total) | ||
+ | { | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | EmitSoundAny(clients, total, sample, entity, channel, | ||
+ | level, flags, volume, pitch, speakerentity, | ||
+ | origin, dir, updatePos, soundtime); | ||
+ | } | ||
+ | |||
+ | stock EmitAmbientSoundAny(const String:name[], | ||
+ | const Float:pos[3], | ||
+ | entity = SOUND_FROM_WORLD, | ||
+ | level = SNDLEVEL_NORMAL, | ||
+ | flags = SND_NOFLAGS, | ||
+ | Float:vol = SNDVOL_NORMAL, | ||
+ | pitch = SNDPITCH_NORMAL, | ||
+ | Float:delay = 0.0) | ||
+ | { | ||
+ | decl String:szSound[PLATFORM_MAX_PATH]; | ||
+ | |||
+ | if (g_bNeedsFakePrecache) | ||
+ | { | ||
+ | Format(szSound, sizeof(szSound), "*%s", sample); | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | strcopy(szSound, sizeof(szSound), sample); | ||
+ | } | ||
+ | |||
+ | EmitAmbientSound(szSound, pos, entity, level, flags, vol, pitch, delay); | ||
+ | }</pawn> | ||
===Using the music directory=== | ===Using the music directory=== |
Revision as of 14:57, 16 March 2014
Others are encouraged to expand on this article with their findings or to clarify any information
This article explains some of the quirks when coding for CS:GO and offers some workarounds when available and other information.
Playing Custom Sounds
Since Left 4 Dead, all normally played sounds must exist in the sound cache on the client. This is an issue for custom sounds as adding to the sound cache requires the client to run snd_rebuildaudiocache (not executable by the server), which also takes a sizable amount of time to run while otherwise locking up the game.
Workarounds
Note that with any of these methods, it is still required that you still add the sound to the download table if you need the client to download it from the server or "fastdl".
All of the listed workarounds involve explicitly telling the client to stream the sound directly from the disk rather than starting it from the cache. This is less than ideal, but seems to work well enough. These have only been made to work with mp3 files. A way to support wav files has not yet been found.
The "play" client command
If you don't need all of the flexibility that the EmitSound* natives expose and just need one or more clients to hear a sound, you can use the ClientCommand native with the 'play' command. The only difference from normal operation is the prefixing of an asterisk (*) to the path which denotes the sound to be streamed. For this method, you do not need to use PrecacheSound on the server.
Example: for csgo/sound/custom/ur.mp3
ClientCommand( client, "play *custom/ur.mp3" );
Fake precaching and EmitSound
For this method, you also need to make use of the asterisk trick. Unfortunately, the EmitSound natives will fail if the file is not listed in the soundprecache table, and PrecacheSound will fail if the file does not exist, (which is won't, as it treats the asterisk as part of the path when doing the lookup).
Since the sound will be streamed, not needing to actually be precached other than to satisfy the check in EmitSound, we can work around this by manually adding the path, with the asterisk prefixed, directly to the soundprecache table. Then you use the EmitSound native of choice as usual with the exception of the asterisk.
Full plugin example:
#include <sourcemod> #include <sdktools> new const String:FULL_SOUND_PATH[] = "sound/custom/ur.mp3"; new const String:RELATIVE_SOUND_PATH[] = "*custom/ur.mp3"; public OnPluginStart() { RegConsoleCmd( "sm_testsound", sm_testsound ); } public OnMapStart() { AddFileToDownloadsTable( FULL_SOUND_PATH ); FakePrecacheSound( RELATIVE_SOUND_PATH ); } public Action:sm_testsound( client, argc ) { EmitSoundToClient( client, RELATIVE_SOUND_PATH ); return Plugin_Handled; } stock FakePrecacheSound( const String:szPath[] ) { AddToStringTable( FindStringTable( "soundprecache" ), szPath ); }
Alternately, if you are going to have sounds in a plugin that supports multiple games, you can easily do it like this, noting that the * is missing from the relative sound path.
Keep in mind that you need to play sounds with one of the EmitSound*Any stocks.
#include <sourcemod> #include <sdktools> new const String:FULL_SOUND_PATH[] = "sound/custom/ur.mp3"; new const String:RELATIVE_SOUND_PATH[] = "custom/ur.mp3"; new bool:g_bNeedsFakePrecache = false; public OnPluginStart() { RegConsoleCmd( "sm_testsound", sm_testsound ); new EngineVersion:engVersion = GetEngineVersion(); if (engVersion == Engine_CSGO || engVersion == Engine_DOTA) // if (engVersion == Engine_CSGO) { g_bNeedsFakePrecache = true; } } public OnMapStart() { AddFileToDownloadsTable( FULL_SOUND_PATH ); PrecacheSoundAny( RELATIVE_SOUND_PATH ); } public Action:sm_testsound( client, argc ) { EmitSoundToClient( client, RELATIVE_SOUND_PATH ); return Plugin_Handled; } stock bool:PrecacheSoundAny( const String:szPath[] ) { if (g_bNeedsFakePrecache) { return FakePrecacheSoundEx(szPath); } else { return PrecacheSound(szPath); } } stock bool:FakePrecacheSoundEx( const String:szPath[] ) { decl String:szPathStar[PLATFORM_MAX_PATH]; Format(szPathStar, sizeof(szPathStar), "*%s", szPath); AddToStringTable( FindStringTable( "soundprecache" ), szPathStar ); return true; } stock EmitSoundAny(const clients[], numClients, const String:sample[], entity = SOUND_FROM_PLAYER, channel = SNDCHAN_AUTO, level = SNDLEVEL_NORMAL, flags = SND_NOFLAGS, Float:volume = SNDVOL_NORMAL, pitch = SNDPITCH_NORMAL, speakerentity = -1, const Float:origin[3] = NULL_VECTOR, const Float:dir[3] = NULL_VECTOR, bool:updatePos = true, Float:soundtime = 0.0) { decl String:szSound[PLATFORM_MAX_PATH]; if (g_bNeedsFakePrecache) { Format(szSound, sizeof(szSound), "*%s", sample); } else { strcopy(szSound, sizeof(szSound), sample); } EmitSound(clients, numClients, szSound, entity, channel, level, flags, volume, pitch, speakerentity, origin, dir, updatePos, soundtime); } stock EmitSoundToClientAny(client, const String:sample[], entity = SOUND_FROM_PLAYER, channel = SNDCHAN_AUTO, level = SNDLEVEL_NORMAL, flags = SND_NOFLAGS, Float:volume = SNDVOL_NORMAL, pitch = SNDPITCH_NORMAL, speakerentity = -1, const Float:origin[3] = NULL_VECTOR, const Float:dir[3] = NULL_VECTOR, bool:updatePos = true, Float:soundtime = 0.0) { new clients[1]; clients[0] = client; /* Save some work for SDKTools and remove SOUND_FROM_PLAYER references */ entity = (entity == SOUND_FROM_PLAYER) ? client : entity; EmitSoundAny(clients, 1, sample, entity, channel, level, flags, volume, pitch, speakerentity, origin, dir, updatePos, soundtime); } stock EmitSoundToAllAny(const String:sample[], entity = SOUND_FROM_PLAYER, channel = SNDCHAN_AUTO, level = SNDLEVEL_NORMAL, flags = SND_NOFLAGS, Float:volume = SNDVOL_NORMAL, pitch = SNDPITCH_NORMAL, speakerentity = -1, const Float:origin[3] = NULL_VECTOR, const Float:dir[3] = NULL_VECTOR, bool:updatePos = true, Float:soundtime = 0.0) { new clients[MaxClients]; new total = 0; for (new i=1; i<=MaxClients; i++) { if (IsClientInGame(i)) { clients[total++] = i; } } if (!total) { return; } EmitSoundAny(clients, total, sample, entity, channel, level, flags, volume, pitch, speakerentity, origin, dir, updatePos, soundtime); } stock EmitAmbientSoundAny(const String:name[], const Float:pos[3], entity = SOUND_FROM_WORLD, level = SNDLEVEL_NORMAL, flags = SND_NOFLAGS, Float:vol = SNDVOL_NORMAL, pitch = SNDPITCH_NORMAL, Float:delay = 0.0) { decl String:szSound[PLATFORM_MAX_PATH]; if (g_bNeedsFakePrecache) { Format(szSound, sizeof(szSound), "*%s", sample); } else { strcopy(szSound, sizeof(szSound), sample); } EmitAmbientSound(szSound, pos, entity, level, flags, vol, pitch, delay); }
Using the music directory
By adding your custom sounds under sound/music/, they will automatically be streamed from disk. They can also be used just like sounds in any other game with regard to PrecacheSound (albeit not necessary to be beyond adding to soundprecache table) and EmitSound.
This will have the side effect of your sounds volume being tied to the game's music volume, which many players turn down or off. For this reason, it's not recommended.
Max Players, Clients
CS:GO has four different values that all affect the maximum number of players that can join a game. The lowest one of the four determines the value used.
The absolute maximum
The current absolute maximum number of players for CS:GO, including GOTV is 64. This is a compile-time maximum in the engine on both client and server and cannot be changed.
(This is the maximum value as returned by IServerGameClients::GetPlayerLimits).
The engine's Maxclients
This is the number known as gpGlobals->maxClients in SM extensions or MM:S plugins and MaxClients in SourcePawn.
It is able to be changed in other games, up to the maximum, by setting the -maxplayers command line parameter. As this doesn't exist in CS:GO, it acts like other games when not set and uses a hardcoded default, 64 for CS:GO.
(This is the default value as returned by IServerGameClients::GetPlayerLimits).
A gamemode's maxplayers can be overridden with the maxplayers parameter in the appropriate section of gamemodes_server.txt
To override maxplayers for all gamemodes, use the -maxplayers_override command line parameter. However, be aware that the game itself appears to enforce a 44 player maximum regardless of what you set this to.
The GameTypes maxplayers
CS:GO has a new "GameTypes" system with it's own set of game mode and type -specifec params. There is more on this below, but it also includes a maxplayers value, also referred to as numSlots or MaxHumanPlayers in some areas.
This is the value set in gamemodes.txt or overridden in gamemodes_server.txt or overridden by the new -maxplayer_override command line parameter.
It is used for showing the maxplayers listed in the output of the status command as well as the max count used for the server browser (unless overridden by sv_visiblemaxplayers).
This does not include the count set by the "extraspectators" value in the gamemodes.txt, but both maxplayers and extraspectators must be equal or less than the engine's Maxclients.
The GameTypes max can be retrieved in SourcePawn with the new GetMaxHumanPlayers native or in C++ with IServerGameClients::GetMaxHumanPlayers.
Spawnpoint count
Regardless of the above values, you're limited by the running map's number of spawnpoints, evenly split per team (30 for stock maps).
You can use a mod like Stripper:Source or Spawn Tools 7 to add more spawnpoint entities.
Menus
With SourceMod's "Radio-style" menus (ShowMenu / CHudMenu), the 0 key will cannot be detected due to the client never sending "menuselect 0". SourceMod works around this by limiting menus to 9.
The older "Valve-style" menus created by IServerPluginHelpers::CreateMessage with the DIALOG_MENU type aren't supported at all due at least to missing res file info on the client.
GameTypes / GameModes
CS:GO uses a new "GameTypes" system for coordinating server mode and type info between the server, client, and matchmaking, as well as for handling some server rules.
Some things handled by the GameTypes system:
- Game types
- Game modes
- Config to execute for each mode
- Weapon progression for applicable modes
- Maps and map order for each mode
- Maxplayers for each type and mode
- Player and view models for each map
- Bot difficulty
- ELO ranking data
gamemodes.txt is the main data file (in KeyValues format) holding the backing info, with an optional gamemodes_server.txt being merged into it.
The gamemodes data files should not be accessed directly, but rather through the IGameTypes interface. None of it is currently exposed in SourceMod but there are plans to add a new extension giving access to much of the data.
In C++, you can easily get a pointer to the gametypes interface with CreateInterfaceFn from the matchmaking_ds binary, looking up VENGINE_GAMETYPES_VERSION.
See: http://hg.alliedmods.net/hl2sdks/hl2sdk-csgo/file/tip/public/matchmaking/igametypes.h
Example for getting the gametypes ptr in an SM extension, courtesy of Drifter's work-in-progress gametypes extension:
IGameTypes *g_pGameTypes; #define GET_V_IFACE_CURRENT_CUSTOM(v_factory, v_var, v_type, v_name) \ v_var = (v_type *)g_SMAPI->VInterfaceMatch(v_factory, v_name); \ if (!v_var) \ { \ if (error && maxlen) \ { \ g_SMAPI->Format(error, maxlen, "Could not find interface: %s", v_name); \ } \ return false; \ } { CreateInterfaceFn matchmakingDSFactory = NULL; ILibrary *mmlib; char path[PLATFORM_MAX_PATH]; libsys->PathFormat(path, sizeof(path), "%s/bin/matchmaking_ds%s.%s", g_SMAPI->GetBaseDir(), MATCHMAKINGDS_SUFFIX, MATCHMAKINGDS_EXT); if ((mmlib = libsys->OpenLibrary(path, NULL, 0))) { matchmakingDSFactory = (CreateInterfaceFn)mmlib->GetSymbolAddress("CreateInterface"); mmlib->CloseLibrary(); } if (!matchmakingDSFactory) { g_pSM->Format(error, maxlen, "Failed to find matchmakingDS factory"); return false; } GET_V_IFACE_CURRENT_CUSTOM(matchmakingDSFactory, g_pGameTypes, IGameTypes, VENGINE_GAMETYPES_VERSION); if (!g_pGameTypes) { g_pSM->Format(error, maxlen, "Failed to find IGameTypes ptr"); return false; } }
Coding
There are exceptionalities for some of the SourcePawn coding conventions in CS:GO.
Slaying players during player_hurt event
CS:GO will crash if you attempt to call ForcePlayerSuicide() during a player_hurt callback. A timer can be used as a workaround.
Example for slaying a team attacker:
public OnPluginStart() { HookEvent("player_hurt", OnPlayerHurt); } public OnPlayerHurt(Handle:event, const String:name[], bool:dontBroadcast) { new victim = GetClientOfUserId(GetEventInt(event,"userid")); new attacker = GetClientOfUserId(GetEventInt(event,"attacker")); if(attacker > 0 && attacker <= MaxClients && IsPlayerAlive(attacker) && GetClientTeam(attacker) == GetClientTeam(victim)) CreateTimer(0.0, SlayTimer, attacker, TIMER_FLAG_NO_MAPCHANGE); } public Action:SlayTimer(Handle:timer, any:client) { ForcePlayerSuicide(client); }
Assists / Score
Assists and score are not named entity properties in CS:GO. To view or alter assists or displayed score, you can use the following natives new in SM 1.5.0-hg3706:
CS_GetClientAssists CS_SetClientAssists CS_GetContributionScore CS_SetContributionScore
Denoting Text-Colors
CSGO has a few odd requirements for properly coloring text in chat.
// \x01 is white. // \x04 is green. //These examples set the first half of the string green, and the second half white. //Normal example: new String:normalColoring = "\x04This half is green,\x01 This half is white."; //CSGO example: new String:csgoColoring = " \x01\x0B\x04This half is green,\x01 This half is white.";
Specific Quirks:
- The string must start with a space.
- Following the space there must be a white color-indicator (\x01).
- Following the \x01, there must be a printable character. \x0B works great because it does not appear in the message.
- After all of these steps you can treat the rest of the string like normal.