[TUT-CS] Changing player models and lowering svc_bad

For scripting examples for learning how to use AMXX.
User avatar
Archive-Poster
Posts: 1229
Gender:
United States of America

[TUT-CS] Changing player models and lowering svc_bad

Post by Archive-Poster »

Update: (07/2011)
You can now download the CS Player Models API with built in svc_bad prevention here:
<to be added>

About

This tutorial is intended for intermediate to advanced AMXX scripters who have to deal with changing player models in Counter-Strike. It will go over the different available methods and their incidence on the commonly known SVC_BAD errors.

SVC_BAD

If you are unaware of it, this error is usually caused by badly formed engine messages. When they arrive, the most likely outcome is your getting kicked out of the server you were playing on, and one of these errors showing up on your console:
Host_Error: Illegible Server Message: SVC_BAD
Host_Error: UserMsg: Not Present on Client ###
Host_Error: CL_ParseServerMessage: Bad server message
And the like...
Normally, reasons of this are:
Network data getting corrupted on the way
MODs or plugins sending a nonexistant/invalid message
MODs or plugins sending messages with an incorrect number of arguments
However, it also shows up when changing lots of player models at once and it's triggered on almost every client for no apparent reason. Probably some kind of glitch that VALVe will never fix, so what can we do about it? Read on...

Natives from the CStrike module

Probably your first attempt at changing player models was using these. Sure, this method has the advantage of simplicity and you only have to learn three natives. But we're gonna see why you shouldn't be using them if your plugin changes many models (e.g. zombie mods).

From looking at the source, we know that it sets the model on the player's InfoBuffer and keeps track of it, nothing wrong with that so far...

Code: Select all

static cell AMX_NATIVE_CALL cs_set_user_model(AMX *amx, cell *params) // cs_set_user_model(index, const model[]); = 2 params
{
	// ...

	g_players[params[1]].SetModel(model);
	g_players[params[1]].SetModelled(true);

	SETCLIENTKEYVALUE(params[1], GETINFOKEYBUFFER(pPlayer), "model", (char*)g_players[params[1]].GetModel());

	// ...
}
But here's the thing: to prevent CS from resetting the custom model, it does its checks on Player PostThink events so that when the model changes the custom one is immediately set back again.

Code: Select all

void PlayerPostThink(edict_t* pPlayer) {
	int entityIndex = ENTINDEX(pPlayer);

	if(g_players[entityIndex].GetModelled()) {
		if (g_players[entityIndex].GetInspectModel() && strcmp(g_players[entityIndex].GetModel(), GETCLIENTKEYVALUE(GETINFOKEYBUFFER(pPlayer), "model")) != 0) {
			//LOG_CONSOLE(PLID, "%s should have model %s and currently has %s", STRING(pPlayer->v.netname), (char*)g_players[entityIndex].GetModel(), GETCLIENTKEYVALUE(GETINFOKEYBUFFER(pPlayer), "model"));
			SETCLIENTKEYVALUE(entityIndex, GETINFOKEYBUFFER(pPlayer), "model", (char*)g_players[entityIndex].GetModel());
			g_players[entityIndex].SetInspectModel(false);
		}
	}
	RETURN_META(MRES_IGNORED);
}
The problem comes on events like round start, when CS sets everyone's model back to the default ones (gign, sas, leet, etc). So if someone has a custom model it goes like this:
Player is given a custom model "zombie" - a message is sent to all players notifying of this
New Round - a lot of messages are sent: update scores, remove entites, spawn players, etc.
CS resets Player's model back to "leet" - another message added to the list
Player's model is changed again instantly on PostThink - yet another one
As you can see, a lot of network traffic goes on when a new round begins. You can even check it out by yourself with plugins like Message Logging by Damaged Soul.

Now, say all 32 players on the server have custom models. If it normally gets to send a lot of stuff when the round starts, now you have to include all the model update messages. Here's when the glitch we previosuly talked about takes part in: it would seem that whenever someone is close to getting a network channel overflow (i.e. receives too much data at once), then [almost] every player is kicked out with SVC_BAD...

Or at least that's what I noticed. And if you're lucky enough to remain unkicked, you may also note that the game world "freezes" for some seconds while the other players are being kicked (like some server side "high CPU usage" kind of lag).

-----

By the way, another issue you may run into when having the CStrike module handle your custom models, is that whenever you use the cs_set_user_team() native on a player, his model gets changed regardless of passing CS_DONTCHANGE as a third argument.

We can see the reason of this by taking a look at the source code again.

Code: Select all

static cell AMX_NATIVE_CALL cs_set_user_team(AMX *amx, cell *params) // cs_set_user_team(index, team, model = 0); = 3 params
{
	// ...

	int model = params[3];

	*((int *)pPlayer->pvPrivateData + OFFSET_TEAM) = params[2];
	if (model != 0)
		*((int *)pPlayer->pvPrivateData + OFFSET_INTERNALMODEL) = model;
	
	// This makes the model get updated right away.
	MDLL_ClientUserInfoChanged(pPlayer, GETINFOKEYBUFFER(pPlayer)); //  If this causes any problems for WON, do this line only in STEAM builds.

	// ...
}
After the team and internal model are set, it makes a call to DLL_ClientUserInfoChanged, which effectively causes CS to reset the player's model.

-----


Workaround #1 - Using Fakemeta

So basically we need to find a way to reproduce the functionality of the previous natives that allows us to have full control over them, to avoid having our models changed all at once on round start. Fakemeta has all we need.

There are 5 things we're gonna have to cover:
Setting a player's model
Retrieving a player's model
Setting back the original (default) model
Preventing CS from changing our custom model
Preventing players from changing our custom model

To start with, you should set up 2 global arrays: the first one will be a boolean indicating whether the player is using a custom model or not, and the second will store the name (string) of the custom model when present.

Code: Select all

new g_has_custom_model[33]
new g_player_model[33][32]
1. To set a model we simply tell the engine to set the new KeyValue pair for the Client:

Code: Select all

stock fm_cs_set_user_model( player, const modelname[] )
{
    // Set new model
    engfunc( EngFunc_SetClientKeyValue, player, engfunc( EngFunc_GetInfoKeyBuffer, player ), "model", modelname )
    
    // Remember this player has a custom model
    g_has_custom_model[player] = true
}
2. To retrieve the model: we could just make use of the cs_get_user_model() native, but if you're planning not to include <cstrike> at all, use this instead: (edit: model is now passed byref)

Code: Select all

stock fm_cs_get_user_model( player, model[], len )
{
    // Retrieve current model
    engfunc( EngFunc_InfoKeyValue, engfunc( EngFunc_GetInfoKeyBuffer, player ), "model", model, len )
}
3. To restore the default model, as we've seen earlier it's enough to tell the engine the Client's UserInfo has changed, and CS will do the job for us:

Code: Select all

stock fm_cs_reset_user_model( player )
{
    // Player doesn't have a custom model any longer
    g_has_custom_model[player] = false
    
    dllfunc( DLLFunc_ClientUserInfoChanged, player, engfunc( EngFunc_GetInfoKeyBuffer, player ) )
}
4. To block CS from changing the model, we make use of the forward FM_SetClientKeyValue:

Code: Select all

public plugin_init()
{
    // ...
    register_forward( FM_SetClientKeyValue, "fw_SetClientKeyValue" )
}

public fw_SetClientKeyValue( id, const infobuffer[], const key[] )
{
    // Block CS model changes
    if ( g_has_custom_model[id] && equal( key, "model" ) )
        return FMRES_SUPERCEDE;
    
    return FMRES_IGNORED;
}
5. However, since we are blocking SetClientKeyValue, CS will no longer prevent a player from changing his model by typing model "modelname" in his console (thanks Cheap_Suit). So we have to take care of this as well:

Code: Select all

public plugin_init()
{
    // ...
    register_forward( FM_SetClientKeyValue, "fw_SetClientKeyValue" )
}

public fw_SetClientKeyValue( id, const infobuffer[], const key[] )
{
    // Block CS model changes
    if ( g_has_custom_model[id] && equal( key, "model" ) )
    {
        // Get current model
        static currentmodel[32]
        fm_cs_get_user_model( id, currentmodel, charsmax( currentmodel ) )
        
        // Check whether it matches the custom model - if not, set it again
        if ( !equal( currentmodel, g_player_model[id] ) )
            fm_cs_set_user_model( id, g_player_model[id] )
        
        return FMRES_SUPERCEDE;
    }
    
    return FMRES_IGNORED;
}
Note that you can also use: set_user_info( player, "model", modelname ) and get_user_info( player, "model", model[], len ) to set and retrieve player models. They seem to work fine too and are easier to type, but this was supposed to be FakeMeta's way so yeah...

Finally, to make sure there is always a delay between model changes, you can use the following code whenever you need to change a player's model:

Code: Select all

#define MODELCHANGE_DELAY 0.5 // delay between model changes
new Float:g_models_targettime // target time for the last model change

public fm_cs_user_model_update( id )
{
    static Float:current_time
    current_time = get_gametime()
    
    // Delay needed?
    if ( current_time - g_models_targettime >= MODELCHANGE_DELAY )
    {
        fm_cs_set_user_model( id )
        g_models_targettime = current_time
    }
    else
    {
        set_task( (g_models_targettime + MODELCHANGE_DELAY) - current_time, "fm_cs_set_user_model", id )
        g_models_targettime = g_models_targettime + MODELCHANGE_DELAY
    }
}
OK, that's about it. Now you can have tasks in your script changing player models one at a time, instead of all of them changed instantly on Round Start. For an example check CS Player Models API (link at the top).

Workaround #2 - Separate entities

Say you already tried implementing the previous method into your plugin and setting longer delays for the tasks, but you're still getting SVC_BAD every now and then. Is there any other solution worth a try? Well, simply don't change any models. Hey, I'm being serious!

This last method consists on making the actual players invisible and having individual entities (which have your custom player models) to follow them and copy their movements. So even though it looks like it, you aren't actually changing player models at all and no SVC_BAD kicking should occur. (idea taken from ChickenMod 1.0.5.1)

Bad news are, the entities could fail to create or just make the server less stable (not to mention that a higher entity count also means higher CPU usage), so it's kinda experimental.

That said, let's get on with it. Note that you'll need 2 entities for every player, since making them invisible also hides their weapons. First, set up 3 global arrays. One to store the model name, while the others will be used to store the index of the entities following the player. Also, add defines for their classnames if you wish.

Code: Select all

new g_player_model[33][32]
new g_ent_playermodel[33]
new g_ent_weaponmodel[33]

new const PLAYERMODEL_CLASSNAME[] = "ent_playermodel"
new const WEAPONMODEL_CLASSNAME[] = "ent_weaponmodel"
Next, this is the function you'll need to call whenever you want to set a custom player model:

Code: Select all

stock fm_set_playermodel_ent( id, const modelname[] )
{
    // Make original player entity invisible
    set_pev( id, pev_rendermode, kRenderTransTexture )
    // This is not 0 because it would hide the shadow and some effects when firing weapons
    set_pev( id, pev_renderamt, 1.0 )
    
    // Since we're passing the short model name to the function
    // we need to make the full path out of it
    static modelpath[100]
    formatex( modelpath, charsmax( modelpath ), "models/player/%s/%s.mdl", modelname, modelname )
    
    // Check if the entity assigned to this player exists
    if ( !pev_valid( g_ent_playermodel[id] ) )
    {
        // If it doesn't, proceed to create a new one
        g_ent_playermodel[id] = engfunc( EngFunc_CreateNamedEntity, engfunc( EngFunc_AllocString, "info_target" ) )
        
        // If it failed to create for some reason, at least this will prevent further "Invalid entity" errors...
        if ( !pev_valid( g_ent_playermodel[id] ) ) return;
        
        // Set its classname
        set_pev( g_ent_playermodel[id], pev_classname, PLAYERMODEL_CLASSNAME )
        
        // Make it follow the player
        set_pev( g_ent_playermodel[id], pev_movetype, MOVETYPE_FOLLOW )
        set_pev( g_ent_playermodel[id], pev_aiment, id )
        set_pev( g_ent_playermodel[id], pev_owner, id )
    }
    
    // Entity exists now, set its model
    engfunc( EngFunc_SetModel, g_ent_playermodel[id], modelpath )
}
This stock will return whether a player is using a custom model:

Code: Select all

stock fm_has_custom_model( id )
{
    return pev_valid( g_ent_playermodel[id] ) ? true : false;
}
This one will be called after a weapon change event to update our "weaponmodel" ent:

Code: Select all

stock fm_set_weaponmodel_ent( id )
{
    // Get the player's p_ weapon model
    static model[100]
    pev( id, pev_weaponmodel2, model, charsmax( model ) )
    
    // Check if the entity assigned to this player exists
    if ( !pev_valid(g_ent_weaponmodel[id]) )
    {
        // If it doesn't, proceed to create a new one
        g_ent_weaponmodel[id] = engfunc( EngFunc_CreateNamedEntity, engfunc( EngFunc_AllocString, "info_target" ) )
        
        // If it failed to create for some reason, at least this will prevent further "Invalid entity" errors...
        if ( !pev_valid( g_ent_weaponmodel[id] ) ) return;
        
        // Set its classname
        set_pev( g_ent_weaponmodel[id], pev_classname, WEAPONMODEL_CLASSNAME )
        
        // Make it follow the player
        set_pev( g_ent_weaponmodel[id], pev_movetype, MOVETYPE_FOLLOW )
        set_pev( g_ent_weaponmodel[id], pev_aiment, id )
        set_pev( g_ent_weaponmodel[id], pev_owner, id )
    }
    
    // Entity exists now, set its model
    engfunc( EngFunc_SetModel, g_ent_weaponmodel[id], model )
}
Finally, this is the function that will get rid of your custom player model: (and entities)

Code: Select all

stock fm_remove_model_ents( id )
{
    // Make the player visible again
    set_pev( id, pev_rendermode, kRenderNormal )
    
    // Remove "playermodel" ent if present
    if ( pev_valid( g_ent_playermodel[id] ) )
    {
        engfunc( EngFunc_RemoveEntity, g_ent_playermodel[id] )
        g_ent_playermodel[id] = 0
    }
    // Remove "weaponmodel" ent if present
    if ( pev_valid( g_ent_weaponmodel[id] ) )
    {
        engfunc( EngFunc_RemoveEntity, g_ent_weaponmodel[id] )
        g_ent_weaponmodel[id] = 0
    }
}
It wasn't that hard right? But there are still some issues left to take care of...

1. When a player dies, the corpse that's spawned on the ground will not have the custom model set. To fix this we need to hook the corpse message like so:

Code: Select all

public plugin_init()
{
    // ...
    register_message( get_user_msgid( "ClCorpse" ), "message_clcorpse" )
}

public message_clcorpse()
{
    // Get player's id
    static id
    id = get_msg_arg_int( 12 )
    
    // Check if the player is using a custom player model
    if ( fm_has_custom_model( id ) )
    {
        // Set correct model on player corpse
        set_msg_arg_string( 1, g_player_model[id] )
    }
}
2. If we want to give the player a different rendering (e.g. glow) we'll need to set it on our custom entity instead:

Code: Select all

// Set a red glow on the "playermodel" entity
set_pev( g_ent_playermodel[id], pev_renderfx, kRenderFxGlowShell )
set_pev( g_ent_playermodel[id], pev_color, Float:{200.0, 0.0, 0.0} )
set_pev( g_ent_playermodel[id], pev_renderamt, 50.0 )

// Or, if you're using fakemeta_util's stock instead:
fm_set_rendering( g_ent_playermodel[id], kRenderFxGlowShell, 200, 0, 0, kRenderNormal, 50 )
3. If the model is big enough, its owner may be able to see part of it even though he shouldn't: (thanks Orangutanz)
Edit: You might not need this one after all, I did some tests and the models were automatically hidden to me. In such case better leave it out, since it's an expensive forward.

Code: Select all

public plugin_init()
{
    // ...
    register_forward( FM_AddToFullPack, "fw_AddToFullPack" )
}

public fw_AddToFullPack( es, e, ent, host, hostflags, player )
{
    // Narrow down our matches a bit
    if ( player ) return FMRES_IGNORED;
    
    // Check if it's one of our custom model ents being sent to its owner
    if ( ent == g_ent_playermodel[host] || ent == g_ent_weaponmodel[host] )
        return FMRES_SUPERCEDE;
    
    return FMRES_IGNORED;
}
Alright, I'm gonna put it all together in a nice example now for clarity:

Code: Select all

#include <amxmodx>
#include <fakemeta>
#include <hamsandwich>
#include <cstrike>

new const ZOMBIE_MODEL[] = "zombie" // The model we're gonna use for zombies
new const PLAYERMODEL_CLASSNAME[] = "ent_playermodel"
new const WEAPONMODEL_CLASSNAME[] = "ent_weaponmodel"

new g_player_model[33][32] // player's model name (string)
new g_ent_playermodel[33] // playermodel entity following this player
new g_ent_weaponmodel[33] // weaponmodel entity following this player
new g_zombie[33] // whether the player is a zombie
new g_glow[33] // whether the player has glow

/*================================================================================
 [Plugin Start]
=================================================================================*/

public plugin_precache()
{
    new modelpath[100]
    formatex( modelpath, charsmax( modelpath ), "models/player/%s/%s.mdl", ZOMBIE_MODEL, ZOMBIE_MODEL )
    engfunc( EngFunc_PrecacheModel, modelpath )
}

public plugin_init()
{
    register_plugin( "Player Model Changer Example", "0.3", "MeRcyLeZZ" )
    
    RegisterHam( Ham_Spawn, "player", "fw_PlayerSpawn", 1 )
    register_forward( FM_AddToFullPack, "fw_AddToFullPack" )

    register_event( "CurWeapon", "event_curweapon", "be", "1=1" )
    register_message( get_user_msgid( "ClCorpse" ), "message_clcorpse" )
    
    register_clcmd( "say /glow", "clcmd_sayglow" )
}

/*================================================================================
 [Player Spawn Event]
=================================================================================*/

public fw_PlayerSpawn( id )
{   
    // Not alive or didn't join a team yet
    if ( !is_user_alive( id ) || !cs_get_user_team( id ) )
        return;
    
    // Set to zombie if on Terrorist team
    g_zombie[id] = cs_get_user_team( id ) == CS_TEAM_T ? true : false;
    
    // Check if the player is a zombie
    if ( g_zombie[id] )
    {
        // Store our custom model in g_player_model[id]
        copy( g_player_model[id], charsmax( g_player_model[] ), ZOMBIE_MODEL )
        
        // Set the model on our playermodel entity
        fm_set_playermodel_ent( id, g_player_model[id] )
    }
    // Not a zombie, but still has a custom model
    else if ( fm_has_custom_model( id ) )
    {
        // Reset it back to default
        fm_remove_model_ents( id )
    }
}

/*================================================================================
 [Add to Full Pack Forward]
=================================================================================*/

public fw_AddToFullPack( es, e, ent, host, hostflags, player )
{
    // Narrow down our matches a bit
    if ( player ) return FMRES_IGNORED;
    
    // Check if it's one of our custom model ents being sent to its owner
    if ( ent == g_ent_playermodel[host] || ent == g_ent_weaponmodel[host] )
        return FMRES_SUPERCEDE;
    
    return FMRES_IGNORED;
}

/*================================================================================
 [Weapon Change Event]
=================================================================================*/

public event_curweapon( id )
{
    // Check if the player is using a custom player model
    if ( fm_has_custom_model( id ) )
    {
        // Update weapon model on entity
        fm_set_weaponmodel_ent( id )
    }
}

/*================================================================================
 [ClCorpse Message]
=================================================================================*/

public message_clcorpse()
{
    // Get player's id
    static id
    id = get_msg_arg_int( 12 )
    
    // Check if the player is using a custom player model
    if ( fm_has_custom_model( id ) )
    {
        // Set correct model on player corpse
        set_msg_arg_string( 1, g_player_model[id] )
    }
}

/*================================================================================
 [Client Disconnect Event]
=================================================================================*/

public client_disconnect( id )
{
    // Check if the player was using a custom player model
    if ( fm_has_custom_model( id ) )
    {
        // Remove custom entities
        fm_remove_model_ents( id )
    }
}

/*================================================================================
 [Client Command: Say /Glow]
=================================================================================*/

public clcmd_sayglow( id )
{
    // Turn glow on/off
    g_glow[id] = !( g_glow[id] )
    
    // Check if the player is using a custom player model
    if ( fm_has_custom_model( id ) )
    {
        // Check if the player has glow
        if ( g_glow[id] )
        {
            // Set glow on playermodel entity
            fm_set_rendering( g_ent_playermodel[id], kRenderFxGlowShell, 200, 0, 0, kRenderNormal, 50 )
        }
        else
        {
            // Remove glow on playermodel entity
            fm_set_rendering( g_ent_playermodel[id] )
        }
    }
    else
    {
        // Set and remove glow the usual way
        if ( g_glow[id] )
        {
            fm_set_rendering( id, kRenderFxGlowShell, 200, 0, 0, kRenderNormal, 50 )
        }
        else
        {
            fm_set_rendering( id )
        }
    }
}

/*================================================================================
 [Stocks]
=================================================================================*/

stock fm_set_playermodel_ent( id, const modelname[] )
{
    // Make original player entity invisible
    set_pev( id, pev_rendermode, kRenderTransTexture )
    // This is not 0 because it would hide the shadow and some effects when firing weapons
    set_pev( id, pev_renderamt, 1.0 )
    
    // Since we're passing the short model name to the function
    // we need to make the full path out of it
    static modelpath[100]
    formatex( modelpath, charsmax( modelpath ), "models/player/%s/%s.mdl", modelname, modelname )
    
    // Check if the entity assigned to this player exists
    if ( !pev_valid( g_ent_playermodel[id] ) )
    {
        // If it doesn't, proceed to create a new one
        g_ent_playermodel[id] = engfunc( EngFunc_CreateNamedEntity, engfunc( EngFunc_AllocString, "info_target" ) )
        
        // If it failed to create for some reason, at least this will prevent further "Invalid entity" errors...
        if ( !pev_valid( g_ent_playermodel[id] ) ) return;
        
        // Set its classname
        set_pev( g_ent_playermodel[id], pev_classname, PLAYERMODEL_CLASSNAME )
        
        // Make it follow the player
        set_pev( g_ent_playermodel[id], pev_movetype, MOVETYPE_FOLLOW )
        set_pev( g_ent_playermodel[id], pev_aiment, id )
        set_pev( g_ent_playermodel[id], pev_owner, id )
    }
    
    // Entity exists now, set its model
    engfunc( EngFunc_SetModel, g_ent_playermodel[id], modelpath )
}

stock fm_has_custom_model( id )
{
    return pev_valid( g_ent_playermodel[id] ) ? true : false;
}

stock fm_set_weaponmodel_ent( id )
{
    // Get the player's p_ weapon model
    static model[100]
    pev( id, pev_weaponmodel2, model, charsmax( model ) )
    
    // Check if the entity assigned to this player exists
    if ( !pev_valid(g_ent_weaponmodel[id]) )
    {
        // If it doesn't, proceed to create a new one
        g_ent_weaponmodel[id] = engfunc( EngFunc_CreateNamedEntity, engfunc( EngFunc_AllocString, "info_target" ) )
        
        // If it failed to create for some reason, at least this will prevent further "Invalid entity" errors...
        if ( !pev_valid( g_ent_weaponmodel[id] ) ) return;
        
        // Set its classname
        set_pev( g_ent_weaponmodel[id], pev_classname, WEAPONMODEL_CLASSNAME )
        
        // Make it follow the player
        set_pev( g_ent_weaponmodel[id], pev_movetype, MOVETYPE_FOLLOW )
        set_pev( g_ent_weaponmodel[id], pev_aiment, id )
        set_pev( g_ent_weaponmodel[id], pev_owner, id )
    }
    
    // Entity exists now, set its model
    engfunc( EngFunc_SetModel, g_ent_weaponmodel[id], model )
}

stock fm_remove_model_ents( id )
{
    // Make the player visible again
    set_pev( id, pev_rendermode, kRenderNormal )
    
    // Remove "playermodel" ent if present
    if ( pev_valid( g_ent_playermodel[id] ) )
    {
        engfunc( EngFunc_RemoveEntity, g_ent_playermodel[id] )
        g_ent_playermodel[id] = 0
    }
    // Remove "weaponmodel" ent if present
    if ( pev_valid( g_ent_weaponmodel[id] ) )
    {
        engfunc( EngFunc_RemoveEntity, g_ent_weaponmodel[id] )
        g_ent_weaponmodel[id] = 0
    }
}

// Set entity's rendering type (from fakemeta_util)
stock fm_set_rendering(entity, fx = kRenderFxNone, r = 255, g = 255, b = 255, render = kRenderNormal, amount = 16)
{
    new Float:color[3]
    color[0] = float(r)
    color[1] = float(g)
    color[2] = float(b)
    
    set_pev(entity, pev_renderfx, fx)
    set_pev(entity, pev_rendercolor, color)
    set_pev(entity, pev_rendermode, render)
    set_pev(entity, pev_renderamt, float(amount))
}
Final Words

That's it. These are probably the best workarounds to SVC_BAD to the date. You can even try including both methods in a single plugin, and adding a define (or CVAR) for server admins to change if they're getting errors or whatever.

Also, feel free to check Zombie Plague Mod for implementation details for using multiple random models.

Thanks for reading

Who is online

Users browsing this forum: No registered users and 0 guests