Page 1 of 1

Automatic func_tanks in Counter Strike

Posted: Sun Sep 13, 2026 3:21 pm
by Archive-Poster
Hello. I was searching for random functions to test with orpheu and got curious about func_tanks. They have auto aiming and shooting functionality but it doesn't work out of the box in CS so I tried to unlock it. The process is easy: we just have to make them think, have basic flags, and override its search for players because it's code inherited from halflife searches for players in PVS but it seems that they don't have one in CS .

The code:

Code: Select all

public plugin_precache()
{
    RegisterHam(Ham_Spawn,"func_tank","spawnTank",1)    
    OrpheuRegisterHook(OrpheuGetEngineFunction("pfnFindClientInPVS","FindClientInPVS"),"OnFindClientInPVS")
}

public spawnTank(id)
{
    set_pev(id,pev_spawnflags, SF_TANK_ACTIVE | SF_TANK_PLAYER | SF_TANK_HUMANS | SF_TANK_CANCONTROL | SF_TANK_SOUNDON)
    dllfunc(DLLFunc_Think,id)
}

public OrpheuHookReturn:OnFindClientInPVS(id)
{
    OrpheuSetReturn(find_ent_by_class(0,"player"))
    return OrpheuSupercede
}
Arkshine told me that the only flag really needed is SF_TANK_ACTIVE and you can see more about them in hlsdk.

If you want to use this code in a zombie server or something like that just change the function OnFindClientInPVS and use on it an algorithm of yours to find a player to shoot at (like the closer zombie or whatever).

Final notes:
It almost could be made without orpheu because fakemeta also provides hooking for FindClientInPVS but I tested it and it didn't get called.

Offsets for CFuncTank - <to be added>

FindClientInPVS can be also called in more situations for other entities (though is unusual) so you might want to add it's hook an extra check to see if it is a func_tank using it.

Thanks to Capostrike for testing with me

Re: Automatic func_tanks in Counter Strike

Posted: Sun Sep 13, 2026 3:31 pm
by Archive-Poster
here useful example:

Code: Select all

#include <amxmodx>
#include <orpheu>
#include <orpheu_stocks>
#include <fakemeta>
#include <hamsandwich>
#include <xs>

#define KILL_ONLYT // Kill only Terrorist Team...
// #define KILL_ONLYCT // Kill only Counter-Terrorist Team...
// #define ZOMBIEPLAGUE // For zombie plague mod...

#if defined ZOMBIEPLAGUE
#include <zombieplague>
#elseif defined KILL_ONLYT || defined KILL_ONLYCT
#include <cstrike>
#endif

public plugin_precache()
{
    register_plugin("Example Advanced Spawn Tank","1.0","capostrike93/joaquim")
    RegisterHam(Ham_Spawn,"func_tank","spawnTank",1)    
    OrpheuRegisterHook(OrpheuGetEngineFunction("pfnFindClientInPVS","FindClientInPVS"),"OnFindClientInPVS")
}

public spawnTank(id)
{
    set_pev(id,pev_spawnflags, SF_TANK_ACTIVE | SF_TANK_PLAYER | SF_TANK_HUMANS | SF_TANK_CANCONTROL | SF_TANK_SOUNDON)
    dllfunc(DLLFunc_Think,id)
    set_pdata_float(id,41,360.0,4)
}

public OrpheuHookReturn:OnFindClientInPVS(id)
{
    OrpheuSetReturn(findCloserPlayer(id))
    return OrpheuSupercede
}

findCloserPlayer(id)
{
    new Float:originFTank[3]
    pev(id,pev_origin,originFTank)
    
    static
    Float:originFPlayer[3],
    Float:Distance,
    Float:CloserDistance;

    // Reset Distance
    CloserDistance = 999999.0;
    
    new CloserPlayer
    
    for(new i;i<33;i++)
    {
        if(!is_user_alive(i)) continue
        #if defined ZP
        if(!zp_get_user_zombie(i)) continue
        #endif
        #if defined KILL_ONLYT
        if(cs_get_user_team(i) != CS_TEAM_T) continue
        #endif
        #if defined KILL_ONLYCT
        if(cs_get_user_team(i) != CS_TEAM_CT) continue
        #endif
        
        pev(i,pev_origin,originFPlayer)
        
        Distance = get_distance_f(originFTank,originFPlayer)
        if(CloserDistance > Distance)
        {
            CloserPlayer = i;
            CloserDistance = Distance;
        }
    }
    
    return CloserPlayer;
} 
set_pdata_float(id,41,360.0,4) // For full rotation (360º)

my example does that attack the player closest to the tank ..
by default only attack to team terrorist

#define KILL_ONLYT

Re: Automatic func_tanks in Counter Strike

Posted: Sun Sep 13, 2026 3:32 pm
by Archive-Poster
It works using fakemeta too

(I'm using random 1-32 because I'm too lazy to make it better )

Code: Select all

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

public plugin_init()
{
        register_forward(FM_FindClientInPVS, "findclientinpvs")
}

public plugin_precache()
{
        RegisterHam(Ham_Spawn,"func_tank","spawnTank",1)
}

public spawnTank(id)
{
        set_pev(id,pev_spawnflags, SF_TANK_ACTIVE | SF_TANK_PLAYER | SF_TANK_HUMANS | SF_TANK_CANCONTROL | SF_TANK_SOUNDON)
        dllfunc(DLLFunc_Think,id)
}

public findclientinpvs(id)
{
        static class[32]
        if(pev_valid(id))
        {
                pev(id, pev_classname, class, charsmax(class))
                if(equal(class, "func_tank"))
                {
                        forward_return(FMV_CELL, random_num(1, 32))
                        return FMRES_SUPERCEDE
                }
        }
        return FMRES_IGNORED
} 
Here's the key name/values used for func_tank
(cs_rats2_final example values)

Code: Select all

model *174
origin -35 576 -198
rotatesound ambience/turretrot1.wav
rendermode 0
renderfx 0
spriteflash sprites/muzzleflash2.spr
bullet 3
maxRange 0
minRange 0
firespread 1
persistence 1
bullet_damage 15
firerate 15
spritescale 1
barrelz 7
barrely 0
barrel 66
pitchtolerance 5
pitchrange 75
pitchrate 500
yawtolerance 15
yawrange 55
yawrate 500
target boom
rendercolor 0 0 0
renderamt 0
targetname gunny
spawnflags 32
well, but it is clear that has to verify the integrity of random_num

I mean a. player is alive? check team? other?

and I almost forgot..

add
set_pdata_float(id,41,360.0,4) // For full rotation (360º)
Only if I need

Btw, I think it's possible to set full rotation using key "yawrange".
ammm xD??

...

m_yawRange is num 41
set_pdata_float(id,41,360.0,4)

Re: Automatic func_tanks in Counter Strike

Posted: Sun Sep 13, 2026 3:38 pm
by Archive-Poster
If you want to use the keyvalue, I guess you need to hook the spawn as pre.

Like Quim said, you can get a list of offsets i the CFuncTank_(CS) link; and you can see the name is enough explicit to understand the keyvalue name related :

Code: Select all

void CFuncTank :: KeyValue( KeyValueData *pkvd )
{
	if (FStrEq(pkvd->szKeyName, "yawrate"))
	{
		m_yawRate = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "yawrange"))
	{
		m_yawRange = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "yawtolerance"))
	{
		m_yawTolerance = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "pitchrange"))
	{
		m_pitchRange = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "pitchrate"))
	{
		m_pitchRate = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "pitchtolerance"))
	{
		m_pitchTolerance = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "firerate"))
	{
		m_fireRate = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "barrel"))
	{
		m_barrelPos.x = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "barrely"))
	{
		m_barrelPos.y = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "barrelz"))
	{
		m_barrelPos.z = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "spritescale"))
	{
		m_spriteScale = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "spritesmoke"))
	{
		m_iszSpriteSmoke = ALLOC_STRING(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "spriteflash"))
	{
		m_iszSpriteFlash = ALLOC_STRING(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "rotatesound"))
	{
		pev->noise = ALLOC_STRING(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "persistence"))
	{
		m_persist = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "bullet"))
	{
		m_bulletType = (TANKBULLET)atoi(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if ( FStrEq(pkvd->szKeyName, "bullet_damage" )) 
	{
		m_iBulletDamage = atoi(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "firespread"))
	{
		m_spread = atoi(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "minRange"))
	{
		m_minRange = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "maxRange"))
	{
		m_maxRange = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "master"))
	{
		m_iszMaster = ALLOC_STRING(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else
		CBaseEntity::KeyValue( pkvd );
}

Re: Automatic func_tanks in Counter Strike

Posted: Sun Sep 13, 2026 3:40 pm
by Archive-Poster
It's more safe to deal with keyvalues than offset since this one can change on update. So if you can, use keyvalues.

Also, if I'm right ( but I've seen some tutorials saying the same thing ), to have a full rotation you should write 180, 180 left + 180 right = 360. Like the Joropito's example : "yawrange 55" ; meaning the range is : 55 + 55 = 110.

Re: Automatic func_tanks in Counter Strike

Posted: Sun Sep 13, 2026 3:42 pm
by Archive-Poster
If you want to use the keyvalue, I guess you need to hook the spawn as pre
Why must be done at precache (in func_tank case)?

This should work (not tested).

Code: Select all

set_kvd(0, KV_ClassName, "func_tank")
set_kvd(0, KV_KeyName, "yawrange")
set_kvd(0, KV_Value, 180)
set_kvd(0, KV_fHandled, 0)
dllfunc(DLLFunc_KeyValue, ent, 0)
In hlsdk I can see that at CFuncTank::KeyValue() every offset is updated with the new values.

Re: Automatic func_tanks in Counter Strike

Posted: Sun Sep 13, 2026 3:43 pm
by Archive-Poster
I have read somewhere after using DispatchKeyValue you must use DispatchSpawn but looking the HLSDK I don't find any reference to that, so I guess it should work right away. I'm going to try to see.
Yeah, maybe that dispatch it's for some entities but for func_tank it seems to be quite different unless CFuncTank::KeyValue isn't related to DLLFunc_KeyValue.

Re: Automatic func_tanks in Counter Strike

Posted: Sun Sep 13, 2026 3:44 pm
by Archive-Poster
You're right, I've tested with this following plugin, type "tank" to spawn an entity in front of you, by default the yaw range is 110°, and if you type "range" it will update the range to 360° and it's well updated right away without needed to spawn again.

Code: Select all

    #include <amxmodx>
    #include <fakemeta>
    #include <hamsandwich>
    #include <engine>
    #include <xs>
    
    const MAX_CLIENTS  = 32;
    const MAX_ENTITIES = ( 900 + ( MAX_CLIENTS * 15 ) ) / 32;

    new isTank[ MAX_ENTITIES ];

    #define SetIsTank(%1)     ( isTank[ %1 >> 5 ] |=  ( 1 << ( %1 & 31 ) ) )
    #define ClearIsTank(%1)   ( isTank[ %1 >> 5 ] &= ~( 1 << ( %1 & 31 ) ) )
    #define IsTank(%1)        ( isTank[ %1 >> 5 ] &   ( 1 << ( %1 & 31 ) ) )

    public plugin_precache()
    {
        register_forward( FM_Spawn, "Entities_Spawn" );
        
        precache_model( "models/turret.mdl" );
        precache_model( "sprites/muzzleflash2.spr" );
        precache_sound( "ambience/turretrot1.wav" );
    }

    public Entities_Spawn ( const entity )
    {
        new classname[ 32 ];
        pev( entity, pev_classname, classname, charsmax( classname ) );

        if ( pev_valid( entity ) )
        {
            equal( classname, "func_tank" ) ? SetIsTank( entity ) : ClearIsTank( entity );
        }
    }

    public plugin_init()
    {
        register_plugin( "", "", "" );
        
        register_forward( FM_FindClientInPVS, "FindClientInPVS" );
        
        register_clcmd( "tank" , "ClientCommand_CreateTank" );
        register_clcmd( "range", "ClientCommand_ChangeRange" );
    }

    public ClientCommand_CreateTank ( const client )
    {
        new Float:origin[ 3 ]
        GetAimOriginDistance( client, origin, 200.0 );
    
        new tank = create_entity( "func_tank" );
        
        engfunc( EngFunc_SetModel, tank, "models/v_m4a1.mdl" );
        engfunc( EngFunc_SetOrigin, tank, origin );

        DispatchKeyValue( tank, "rotatesound"  , "ambience/turretrot1.wav"  );
        DispatchKeyValue( tank, "spriteflash"  , "sprites/muzzleflash2.spr" );
        DispatchKeyValue( tank, "spritescale"  , "1" );
        DispatchKeyValue( tank, "bullet"       , "1" );
        DispatchKeyValue( tank, "firespread"   , "1" );
        DispatchKeyValue( tank, "persistence"  , "1" );
        DispatchKeyValue( tank, "bullet_damage", "1" );
        DispatchKeyValue( tank, "firerate"     , "15"  );
        DispatchKeyValue( tank, "yawrate"      , "500" );
        DispatchKeyValue( tank, "yawrange"     , "55"  );
        DispatchKeyValue( tank, "yawtolerance" , "5"   );
        DispatchKeyValue( tank, "pitchrate"    , "500" );
        DispatchKeyValue( tank, "pitchrange"   , "55"  );
        DispatchKeyValue( tank, "pitchtolerance", "5"  );
        DispatchKeyValue( tank, "barrelz"      , "4"   );
        DispatchKeyValue( tank, "barrely"      , "0"   );
        DispatchKeyValue( tank, "barrel"       , "30"  );
        
        set_pev( tank, pev_spawnflags, SF_TANK_ACTIVE );
        DispatchSpawn( tank );
    }
    
    public ClientCommand_ChangeRange ( const client )
    {
        new tank = FM_NULLENT;
        
        while ( ( tank = find_ent_by_class( tank, "func_tank" ) ) )
        {
            DispatchKeyValue( tank, "yawrange", "180" );
        }
        
        return PLUGIN_HANDLED;
    }   

    public FindClientInPVS ( const entity )
    {
        if ( IsTank( entity ) )
        {
            forward_return( FMV_CELL, find_ent_by_class( 0, "player" ) );
            return FMRES_SUPERCEDE;
        }
        
        return FMRES_IGNORED;
    }
    
    GetAimOriginDistance( index, Float:origin[ 3 ], Float:distance ) 
    {
        new Float:start[ 3 ];
        new Float:view_ofs[ 3 ];
        
        pev( index, pev_origin, start );
        pev( index, pev_view_ofs, view_ofs );
        
        xs_vec_add( start, view_ofs, start );

        new Float:dest[ 3 ];
        pev( index, pev_v_angle, dest );
        
        engfunc( EngFunc_MakeVectors, dest );
        global_get( glb_v_forward, dest );
        
        xs_vec_mul_scalar( dest, distance, dest );
        xs_vec_add( start, dest, dest );

        engfunc( EngFunc_TraceLine, start, dest, IGNORE_MONSTERS, index, 0 );
        get_tr2( 0, TR_vecEndPos, origin );
    } 
Nice plugin Arkshine!
Is it possible to make it be "unfriendly" to specific players?
This plugin was just a test, it should not be used like that, but for what you ask, you have to do like in #2, looping through all players and adding your filters instead of using directly find_ent_by_class().
It only target 1 player.
when the player dead, tank still target that player
See the second post. You have to filter. It was just an example.