Загрузка данных


#include < amxmisc >
#include < fakemeta >
#include < hamsandwich >
#include < json >
#include < reapi >

native bio_get_user_money( id );
native bio_set_user_money( id, amount, bool: flash = true );

#if AMXX_VERSION_NUM < 190
    #assert Outdated version of AMX Mod X (AMXX_VERSION_NUM), please update in order to use this plugin properly.
#endif

#pragma semicolon 1

#include < bitmask >

new const TEXT_MESSAGE[ ][ ] =
{
    "#Game_Commencing",
    "#Cstrike_Tutor_Round_Over",
    "#Round_Draw",
    "#Game_will_restart_in",

    "#CTs_Win",
    "#Terrorists_Win"
};

new const SEND_AUDIO[ ][ ] =
{
    "%!MRAD_rounddraw",
    "%!MRAD_terwin",
    "%!MRAD_ctwin"
};

new const ZOMBIE_SCREAM[ ][ ] =
{
    "biohazard/scream1.wav",
    "biohazard/scream2.wav",
    "biohazard/scream3.wav"
};

new const IMMUTABLE_CVARS[ ][ ][ ] = 
{
    { "mp_limitteams", "0" },
    { "mp_autoteambalance", "0" },
    { "mp_round_infinite", "b" },
    { "mp_roundover", "1" },
    { "mp_auto_join_team", "1" },
    { "humans_join_team", "CT" },
    { "mp_autokick", "0" }
};
new m_cImmutableCvars[ sizeof( IMMUTABLE_CVARS ) ];

enum ( += 100 )
{
    TASKID_WARMUP,
    TASKID_LAUNCHGAME,
    TASKID_AFK_CHECK
};

enum _: ClassData
{
    ClassName[ 32 ],
    Float: ClassHealth,
    Float: ClassSpeed,
    Float: ClassGravity,
    ClassModel[ 64 ],
    ClassClaw[ 64 ]
};
new Array: m_arrClassesVec = Invalid_Array,
    m_iNumClasses;

new m_iMenuPosition[ MAX_CLIENTS + 1 ];

new Array: m_arrSpawnPoints = Invalid_Array,
    m_iLastSpawn;

// Plugin's hash maps.
new Trie: m_trUselessEntities = Invalid_Trie,
    Trie: m_trTextMessage = Invalid_Trie,
    Trie: m_trSendAudio = Invalid_Trie;

// Plugin's vars.
new bool: m_bWarmup = true,
    bool: m_bZombieRespawnAllowed;

// Plugin's cvars.
new m_pCvar_FreezeTime,
    m_pCvar_RoundTime,
    m_pCvar_OverHumansWin,
    m_pCvar_ZombieRespawnTime,
    m_pCvar_ZombieRespawnMinDistance,
    m_pCvar_ZombieRespawnLimit,
    m_pCvar_ZombieRespawnChanceDecay,
    m_pCvar_ZombieBalanceAfkTime,
    m_pCvar_MoneyDamageRatio,
    m_pCvar_MoneyInfection;

// Plugin's forwards.
new m_fwdSpawn;

// Client's vars.
new m_varClass[ MAX_CLIENTS + 1 ],
    m_iZombieRespawn[ MAX_CLIENTS + 1 ],
    m_iMoney[ MAX_CLIENTS + 1 ];

// Client's bits.
new m_bitZombie,
    m_bitZombieRespawn,
    m_bitJoinAsZombie;

public plugin_precache( )
{
    register_plugin( "Biohazard", "0.0.1", "oxsi. x Vision" );

    m_arrClassesVec = ArrayCreate( ClassData );
    m_arrSpawnPoints = ArrayCreate( 1, 0 );

    m_trTextMessage = TrieCreate( );
    for ( new i = 0; i < sizeof( TEXT_MESSAGE ); i++ )
        TrieSetCell( m_trTextMessage, TEXT_MESSAGE[ i ], 0 );

    m_trSendAudio = TrieCreate( );
    for ( new i = 0; i < sizeof( SEND_AUDIO ); i++ )
        TrieSetCell( m_trSendAudio, SEND_AUDIO[ i ], 0 );

    for ( new i = 0; i < sizeof( ZOMBIE_SCREAM ); i++ )
        precache_sound( ZOMBIE_SCREAM[ i ] );

    for ( new i = 0; i < sizeof( IMMUTABLE_CVARS ); i++ )
    {
        m_cImmutableCvars[ i ] = get_cvar_pointer( IMMUTABLE_CVARS[ i ][ 0 ] );

        set_pcvar_string( m_cImmutableCvars[ i ], IMMUTABLE_CVARS[ i ][ 1 ] );
        hook_cvar_change( m_cImmutableCvars[ i ], "@Hook_ForcingCvarChange" );
    }

    m_pCvar_FreezeTime = get_cvar_pointer( "mp_freezetime" );
    m_pCvar_RoundTime = get_cvar_pointer( "mp_roundtime" );

    set_pcvar_flags( m_pCvar_FreezeTime, get_pcvar_flags( m_pCvar_FreezeTime ) | FCVAR_UNLOGGED );
    set_pcvar_flags( m_pCvar_RoundTime, get_pcvar_flags( m_pCvar_RoundTime ) | FCVAR_UNLOGGED );

    if ( m_bWarmup )
    {
        set_pcvar_num( m_pCvar_FreezeTime, 0 );
        set_pcvar_float( m_pCvar_RoundTime, 20.0 / 60.0 );

        set_task_ex( 1.0, "@Task_Warmup", TASKID_WARMUP, .flags = SetTask_Repeat );
    }
    else
    {
        set_pcvar_num( m_pCvar_FreezeTime, 15 );
        set_pcvar_float( m_pCvar_RoundTime, 180.0 / 60.0 );
    }

    if ( !Read_JsonField( "biohazard" ) )
        set_fail_state( "Failed read '%s' file." );
}

public plugin_init( )
{
    m_pCvar_OverHumansWin = register_cvar( "bio_over_humans_win", "1" );
    m_pCvar_ZombieRespawnTime = register_cvar( "bio_zombie_respawn", "5.0" );
    m_pCvar_ZombieRespawnMinDistance = register_cvar( "bio_zombie_respawn_min_distance", "1200.0" );
    m_pCvar_ZombieRespawnLimit = register_cvar( "bio_zombie_respawn_limit", "4" );
    m_pCvar_ZombieRespawnChanceDecay = register_cvar( "bio_zombie_chance_decay", "25.0" );
    m_pCvar_ZombieBalanceAfkTime = register_cvar( "bio_balance_afk_time", "45.0" );
    m_pCvar_MoneyDamageRatio = register_cvar( "bio_money_damage_ratio", "0.5" );
    m_pCvar_MoneyInfection = register_cvar( "bio_money_infection", "150" );
    
    register_clcmd( "say /class", "@Open_ClassesMenu" );
    register_clcmd( "say_team /class", "@Open_ClassesMenu" );

    register_menucmd( register_menuid( "@Display_ClassesMenu" ), ( 1 << 0 | 1 << 1 | 1 << 2 | 1 << 3 | 1 << 4 | 1 << 5 | 1 << 6 | 1 << 7 | 1 << 8 | 1 << 9 ), "@Handle_ClassesMenu" );

    register_message( get_user_msgid( "TextMsg" ), "@Message_TextMsg" );
    register_message( get_user_msgid( "SendAudio" ), "@Message_SendAudio" );
    register_message( get_user_msgid( "Money" ), "@Message_Money" );

    RegisterHookChain( RH_SV_ActivateServer, "@SV_ActivateServer_Post", true );

    RegisterHookChain( RG_RoundEnd, "@RoundEnd_Pre" );
    RegisterHookChain( RG_RoundEnd, "@RoundEnd_Post", true );

    RegisterHookChain( RG_CSGameRules_RestartRound, "@CSGameRules_RestartRound_Pre" );
    RegisterHookChain( RG_CSGameRules_RestartRound, "@CSGameRules_RestartRound_Post", true );
    RegisterHookChain( RG_CSGameRules_CheckWinConditions, "@CSGameRules_CheckWinConditions_Pre" );
    RegisterHookChain( RG_CSGameRules_FPlayerCanTakeDamage, "@CSGameRules_FPlayerCanTakeDamage_Pre" );
    RegisterHookChain( RG_CSGameRules_OnRoundFreezeEnd, "@CSGameRules_OnRoundFreezeEnd_Pre" );
    RegisterHookChain( RG_CSGameRules_GetPlayerSpawnSpot, "@CSGameRules_GetPlayerSpawnSpot_Pre" );

    RegisterHookChain( RG_CBasePlayer_Spawn, "@CBasePlayer_Spawn_Pre" );
    RegisterHookChain( RG_CBasePlayer_Spawn, "@CBasePlayer_Spawn_Post", true );
    RegisterHookChain( RG_CBasePlayer_HasRestrictItem, "@CBasePlayer_HasRestrictItem_Pre" );
    RegisterHookChain( RG_CBasePlayer_TakeDamage, "@CBasePlayer_TakeDamage_Pre" );
    RegisterHookChain( RG_CBasePlayer_ResetMaxSpeed, "@CBasePlayer_ResetMaxSpeed_Pre" );
    RegisterHookChain( RG_CBasePlayer_Killed, "@CBasePlayer_Killed_Post", true );

    @Forcing_LevelInitialize( );
}

public plugin_natives( )
{
    register_native( "bio_get_user_money", "@native_get_user_money" );
    register_native( "bio_set_user_money", "@native_set_user_money" );
}

public plugin_end( )
{
    if ( m_trUselessEntities != Invalid_Trie )
    {
        TrieDestroy( m_trUselessEntities );
        m_trUselessEntities = Invalid_Trie;
    }

    if ( m_trTextMessage != Invalid_Trie )
    {
        TrieDestroy( m_trTextMessage );
        m_trTextMessage = Invalid_Trie;
    }

    if ( m_trSendAudio != Invalid_Trie )
    {
        TrieDestroy( m_trSendAudio );
        m_trSendAudio = Invalid_Trie;
    }

    if ( m_arrClassesVec != Invalid_Array )
    {
        ArrayDestroy( m_arrClassesVec );
        m_arrClassesVec = Invalid_Array;
    }
}

public client_putinserver( id )
{
    m_varClass[ id ] = 0;

    BIT_SUB( m_bitJoinAsZombie, BIT_PLAYER( id ) );

    RequestFrame( "@RequestFrame_TryJoinAsZombie", id );
}

public client_disconnected( id )
{
    m_iZombieRespawn[ id ] = 0;
    m_varClass[ id ] = 0;

    BIT_SUB( m_bitZombieRespawn, BIT_PLAYER( id ) );
    BIT_SUB( m_bitZombie, BIT_PLAYER( id ) );
    BIT_SUB( m_bitJoinAsZombie, BIT_PLAYER( id ) );
}

@Open_ClassesMenu( id )
{
    if ( !m_iNumClasses )
    {
        client_print( id, print_chat, "Классы зомби не найдены!" );
        return PLUGIN_HANDLED;
    }

    return @Display_ClassesMenu( id, m_iMenuPosition[ id ] = 0 );
}

@Display_ClassesMenu( id, pos )
{
    if ( pos < 0 )
        return PLUGIN_HANDLED;

    new start;
    if ( ( start = pos * 7 ) > m_iNumClasses )
        start = m_iNumClasses;

    start = start - ( start % 7 );
    pos = start / 7;

    new end;
    if ( ( end = start + 7 ) > m_iNumClasses )
        end = m_iNumClasses;

    new menu[ 512 ], classData[ ClassData ], page = ( m_iNumClasses / 7 + ( ( m_iNumClasses % 7 ) ? 1 : 0 ) ), key = ( 1 << 9 ), k,
    len = add( menu, charsmax( menu ), fmt( "\yClasses %d/%d^n^n", pos + 1, page ) );

    while ( start < end )
    {
        ArrayGetArray( m_arrClassesVec, start, classData );

        if ( m_varClass[ id ] != start )
            key |= ( 1 << k );

        if ( m_varClass[ id ] == start )
            len += add( menu, charsmax( menu ) - len, fmt( "\y%d. \d%s^n", ++k, classData[ ClassName ] ) );
        else
            len += add( menu, charsmax( menu ) - len, fmt( "\y%d. \w%s^n", ++k, classData[ ClassName ] ) );

        start++;
    }

    if ( pos )
        key |= ( 1 << 7 );

    if ( end < m_iNumClasses )
        key |= ( 1 << 8 );

    len += add( menu, charsmax( menu ) - len, fmt( "^n\y8. %sНазад", pos ? "\w" : "\d" ) );
    len += add( menu, charsmax( menu ) - len, fmt( "^n\y9. %sВперёд", end < m_iNumClasses ? "\w" : "\d" ) );
    add( menu, charsmax( menu ), "^n\y0. \wВыход" );
    return show_menu( id, key, menu, -1, "@Display_ClassesMenu" );
}

@Handle_ClassesMenu( id, key )
{
    switch ( key )
    {
        case 7: @Display_ClassesMenu( id, --m_iMenuPosition[ id ] );
        case 8: @Display_ClassesMenu( id, ++m_iMenuPosition[ id ] );
        case 9: return PLUGIN_HANDLED;
        default:
        {
            new class = m_iMenuPosition[ id ] * 7 + key;
            if ( class >= m_iNumClasses )
                return PLUGIN_HANDLED;

            m_varClass[ id ] = class;

            new classData[ ClassData ];
            ArrayGetArray( m_arrClassesVec, class, classData );
            client_print( id, print_chat, "Вы выбрали класс %s %d", classData[ ClassName ], m_varClass[ id ] );
        }
    }

    return PLUGIN_CONTINUE;
}

@Hook_ForcingCvarChange( pcvar, old_value[ ], new_value[ ] )
{
    for ( new i = 0; i < sizeof( IMMUTABLE_CVARS ); i++ )
    {
        if ( m_cImmutableCvars[ i ] != pcvar )
            continue;

        if ( equal( new_value, IMMUTABLE_CVARS[ i ][ 1 ] ) )
            continue;

        set_pcvar_string( pcvar, IMMUTABLE_CVARS[ i ][ 1 ] );
        break;
    }
}

@Message_TextMsg( msg_id, msg_dest, receiver )
{
    new text[ 32 ], value;
    get_msg_arg_string( 2, text, charsmax( text ) );

    if ( TrieGetCell( m_trTextMessage, text, value ) )
        return PLUGIN_HANDLED;

    return PLUGIN_CONTINUE;
}

@Message_SendAudio( msg_id, msg_dest, receiver )
{
    new text[ 32 ], value;
    get_msg_arg_string( 2, text, charsmax( text ) );

    if ( TrieGetCell( m_trSendAudio, text, value ) )
        return PLUGIN_HANDLED;

    return PLUGIN_CONTINUE;
}

@Message_Money( msg_id, msg_dest, receiver )
{
    set_member( receiver, m_iAccount, m_iMoney[ receiver ] );
    set_msg_arg_int( 1, get_msg_argtype( 1 ), m_iMoney[ receiver ] );
}

@Task_Warmup( task )
{
    if ( !get_member_game( m_bRoundTerminating ) )
    {
        new timeLeft = floatround( ( float( get_member_game( m_iRoundTimeSecs ) ) - get_gametime( ) + Float: get_member_game( m_fRoundStartTimeReal ) ) );
        
        if ( timeLeft == 11 )
            client_cmd( 0, "spk ^"plats/elevbell1.wav^"" );
        else if ( timeLeft <= 4 && timeLeft > 1 )
            client_cmd( 0, "spk ^"buttons/bell1.wav^"" );

        if ( timeLeft < 1 )
            rg_round_end( 3.0, WINSTATUS_DRAW, ROUND_GAME_COMMENCE, .trigger = true );
    }
}

@SV_ActivateServer_Post( )
{
    if ( m_fwdSpawn )
    {
        unregister_forward( FM_Spawn, m_fwdSpawn );
        m_fwdSpawn = 0;

        TrieDestroy( m_trUselessEntities );
        m_trUselessEntities = Invalid_Trie;
    }
}

@RoundEnd_Pre( WinStatus: status, ScenarioEventEndRound: event, Float: delay )
{
    if ( m_bWarmup )
    {
        SetHookChainArg( 2, ATYPE_INTEGER, ROUND_GAME_COMMENCE );
        SetHookChainArg( 3, ATYPE_FLOAT, 3.0 );
        return;
    }

    if ( !get_member_game( m_bGameStarted ) )
        return;

    stop_zombie_respawn( );

    if ( status == WINSTATUS_NONE )
        status = WINSTATUS_DRAW;

    if ( status == WINSTATUS_DRAW )
    {
        if ( get_pcvar_bool( m_pCvar_OverHumansWin ) )
            status = WINSTATUS_CTS;
    }

    SetHookChainArg( 1, ATYPE_INTEGER, status );
}

@RoundEnd_Post( WinStatus: status, ScenarioEventEndRound: event, Float: delay )
{
    if ( m_bWarmup )
        return;

    switch ( status )
    {
        case WINSTATUS_CTS:
        {
            client_print( 0, print_center, "Humans won!" );
            client_cmd( 0, " spk ^"ambience/3dmstart.wav^"" );
        }
        case WINSTATUS_TERRORISTS:
        {
            client_print( 0, print_center, "Zombies won!" );
            client_cmd( 0, "spk ^"ambience/the_horror1.wav^"" );
        }
        case WINSTATUS_DRAW:
        {
            client_print( 0, print_center, "No one won!" );
            client_cmd( 0, "spk ^"ambience/3dmstart.wav^"" );
        }
    }
}

@CSGameRules_RestartRound_Pre( )
{
    if ( m_bWarmup )
    {
        m_bWarmup = false;
        remove_task( TASKID_WARMUP );
    }

    set_pcvar_num( m_pCvar_FreezeTime, 15 );
    set_pcvar_float( m_pCvar_RoundTime, 180.0 / 60.0 );
}

@CSGameRules_RestartRound_Post( )
{
    if ( !m_bWarmup )
        set_member_game( m_bGameStarted, true );

    stop_zombie_respawn( );

    for ( new i = 1; i <= MaxClients; i++ )
    {
        if ( !is_user_connected( i ) )
            continue;

        m_iZombieRespawn[ i ] = 0;

        BIT_SUB( m_bitZombie, BIT_PLAYER( i ) );
        BIT_SUB( m_bitZombieRespawn, BIT_PLAYER( i ) );
        set_member( i, m_flRespawnPending, 0.0 );
    }

    @Forcing_LevelInitialize( );
}

@CSGameRules_CheckWinConditions_Pre( )
{
    if ( m_bWarmup )
        return HC_SUPERCEDE;

    if ( !get_member_game( m_bGameStarted ) )
        return HC_SUPERCEDE;

    if ( get_member_game( m_bRoundTerminating ) )
        return HC_SUPERCEDE;

    return HC_SUPERCEDE;
}

@CSGameRules_FPlayerCanTakeDamage_Pre( id, attacker )
{
    if ( get_member_game( m_bRoundTerminating ) )
    {
        SetHookChainReturn( ATYPE_INTEGER, false );
        return HC_SUPERCEDE;
    }

    if ( m_bWarmup )
    {
        SetHookChainReturn( ATYPE_INTEGER, false );
        return HC_SUPERCEDE;
    }

    return HC_CONTINUE;
}

@CSGameRules_OnRoundFreezeEnd_Pre( )
{
    if ( !get_member_game( m_bGameStarted ) )
        return;

    m_bZombieRespawnAllowed = true;

    new Array: alives = ArrayCreate( 1, 0 );
    for ( new i = 1; i <= MaxClients; i++ )
    {
        if ( !is_user_alive( i ) )
            continue;

        ArrayPushCell( alives, i );
    }

    new num = ArraySize( alives );
    if ( num >= 2 )
    {
        new need_zombies = floatround( float( num ) * 0.20, floatround_ceil );

        new id, pos;
        while ( need_zombies-- )
        {
            pos = random( ArraySize( alives ) );
            id = ArrayGetCell( alives, pos );
            ArrayDeleteItem( alives, pos );

            @ZombieAttributes( id, 0 );
        }
    }

    ArrayDestroy( alives );
}

@CSGameRules_GetPlayerSpawnSpot_Pre( id )
{
    if ( TEAM_TERRORIST > get_member( id, m_iTeam ) > TEAM_CT )
        return HC_CONTINUE;

    new spot;
    if ( BIT_VALID( m_bitZombie, BIT_PLAYER( id ) ) )
        spot = @Select_ZombieSpawnPoint( id );
    else
        spot = @Select_SpawnPoint( id );

    if ( is_nullent( spot ) )
        return HC_CONTINUE;

    new Float: vecOrigin[ 3 ], Float: vecAngles[ 3 ];
    get_entvar( spot, var_origin, vecOrigin );
    get_entvar( spot, var_angles, vecAngles );

    vecOrigin[ 2 ] += 1.0;
    set_entvar( id, var_origin, vecOrigin );
    set_entvar( id, var_v_angle, NULL_VECTOR );
    set_entvar( id, var_velocity, NULL_VECTOR );
    set_entvar( id, var_angles, vecAngles );
    set_entvar( id, var_punchangle, NULL_VECTOR );
    set_entvar( id, var_fixangle, 1 );

    SetHookChainReturn( ATYPE_INTEGER, spot );
    return HC_SUPERCEDE;
}

@CBasePlayer_Spawn_Pre( id )
{
    if ( get_member( id, m_bJustConnected ) )
        return;

    if ( TEAM_TERRORIST > get_member( id, m_iTeam ) > TEAM_CT )
        return;

    RequestFrame( "@RequestFrame_ApplyPlayerAttributes", id );
}

@CBasePlayer_Spawn_Post( id )
{
    if ( get_member( id, m_bJustConnected ) )
    {
        set_member( id, m_fNextSuicideTime, 999999999.0 );
        return;
    }

    if ( TEAM_TERRORIST > get_member( id, m_iTeam ) > TEAM_CT )
        return;

    if ( BIT_VALID( m_bitJoinAsZombie, BIT_PLAYER( id ) ) )
    {
        BIT_SUB( m_bitJoinAsZombie, BIT_PLAYER( id ) );

        @ZombieAttributes( id, 0 );
        return;
    }

    if ( get_member_game( m_bFreezePeriod ) )
        set_member( id, m_bCanShootOverride, true );

    if ( BIT_VALID( m_bitZombieRespawn, BIT_PLAYER( id ) ) )
        BIT_SUB( m_bitZombieRespawn, BIT_PLAYER( id ) );
}

@CBasePlayer_HasRestrictItem_Pre( id )
{
    if ( get_member( id, m_iTeam ) != TEAM_TERRORIST )
        return HC_CONTINUE;

    SetHookChainReturn( ATYPE_BOOL, true );
    return HC_SUPERCEDE;
}

@CBasePlayer_TakeDamage_Pre( victim, inflictor, attacker, Float: damage, damage_type )
{
    if ( victim == attacker || !is_user_connected( attacker ) )
        return;

    if ( !rg_is_player_can_takedamage( victim, attacker ) )
        return;

    if ( BIT_VALID( m_bitZombie, BIT_PLAYER( attacker ) ) )
    {
        new activeItem = get_member( attacker, m_pActiveItem );
        if ( !is_nullent( activeItem ) && get_member( activeItem, m_iId ) == WEAPON_KNIFE )
        {
            @ZombieAttributes( victim, attacker );
            bio_set_user_money( attacker, bio_get_user_money( attacker ) + get_pcvar_num( m_pCvar_MoneyInfection ) );

            SetHookChainArg( 4, ATYPE_FLOAT, 0.0 );
            return;
        }
    }

    new moneyRatio = floatround( damage * get_pcvar_float( m_pCvar_MoneyDamageRatio ), floatround_floor );
    bio_set_user_money( attacker, bio_get_user_money( attacker ) + moneyRatio );
}

@CBasePlayer_ResetMaxSpeed_Pre( id )
{
    if ( !is_user_alive( id ) )
        return HC_CONTINUE;

    if ( BIT_VALID( m_bitZombie, BIT_PLAYER( id ) ) )
    {
        new classData[ ClassData ];
        ArrayGetArray( m_arrClassesVec, m_varClass[ id ], classData );

        set_entvar( id, var_maxspeed, classData[ ClassSpeed ] );
        return HC_SUPERCEDE;
    }

    new activeItem = get_member( id, m_pActiveItem ), Float: speed;
    if ( !is_nullent( activeItem ) )
        ExecuteHamB( Ham_CS_Item_GetMaxSpeed, activeItem, speed );
    else
        speed = 240.0;

    set_entvar( id, var_maxspeed, speed );
    return HC_SUPERCEDE;
}

@CBasePlayer_Killed_Post( id, attacker, gib )
{
    if ( is_user_alive( id ) )
        return;

    new TeamName: team = get_member( id, m_iTeam );
    if ( team == TEAM_SPECTATOR || team == TEAM_UNASSIGNED )
        return;

    if ( get_member_game( m_bRoundTerminating ) )
        return;

    if ( !get_member_game( m_bGameStarted ) )
        return;

    if ( BIT_VALID( m_bitZombie, BIT_PLAYER( id ) ) )
    {
        if ( m_bZombieRespawnAllowed && CanZombieRespawn( id ) )
        {
            new Float: respawnTime = get_pcvar_float( m_pCvar_ZombieRespawnTime );
            if ( respawnTime > 0.0 )
            {
                BIT_ADD( m_bitZombieRespawn, BIT_PLAYER( id ) );
                m_iZombieRespawn[ id ]++;

                set_member( id, m_flRespawnPending, get_gametime( ) + respawnTime );
                return;
            }
        }
    }

    RequestFrame( "@RequestFrame_CheckChangeClassWinConditions" );
}

@FM_Spawn_Pre( entity )
{
    new className[ 32 ];
    get_entvar( entity, var_classname, className, charsmax( className ) );

    if ( TrieKeyExists( m_trUselessEntities, className ) )
    {
        forward_return( FMV_CELL, NULLENT );
        return FMRES_SUPERCEDE;
    }

    return FMRES_IGNORED;
}

@ZombieAttributes( id, attacker )
{
    BIT_ADD( m_bitZombie, BIT_PLAYER( id ) );

    new classData[ ClassData ];
    ArrayGetArray( m_arrClassesVec, m_varClass[ id ], classData );

    set_entvar( id, var_health, classData[ ClassHealth ] );
    set_entvar( id, var_gravity, classData[ ClassGravity ] );

    set_entvar( id, var_armorvalue, 0.0 );
    set_member( id, m_iKevlar, ARMOR_NONE );

    rg_set_user_model( id, classData[ ClassModel ] );

    rg_remove_all_items( id );
    rg_give_item( id, "weapon_knife" );

    set_entvar( id, var_viewmodel, classData[ ClassClaw ] );
    set_entvar( id, var_weaponmodel, NULL_STRING );

    rh_emit_sound2( id, 0, CHAN_VOICE, ZOMBIE_SCREAM[ random( sizeof( ZOMBIE_SCREAM ) ) ] );

    if ( attacker )
    {
        @UTIL_DeathMsg( attacker, id, 0, "infection" );
        @UTIL_ScoreAttrib( id, 0 );

        rg_set_user_team( id, TEAM_TERRORIST );

        ExecuteHamB( Ham_AddPoints, id, 0, true );
        ExecuteHamB( Ham_AddPoints, attacker, 1, true );

        set_member( id, m_iDeaths, get_member( id, m_iDeaths ) + 1 );
    }
    else
    {
        @UTIL_DeathMsg( id, id, 0, "infection" );
        @UTIL_ScoreAttrib( id, 0 );

        rg_set_user_team( id, TEAM_TERRORIST );
    }

    if ( get_member_game( m_bGameStarted ) && !get_member_game( m_bFreezePeriod ) )
        RequestFrame( "@RequestFrame_CheckChangeClassWinConditions" );
}

@HuamnAttributes( id, attacker )
{
    rg_set_user_team( id, TEAM_CT );
    BIT_SUB( m_bitZombie, BIT_PLAYER( id ) );

    set_entvar( id, var_health, 100.0 );
    set_entvar( id, var_gravity, 1.0 );

    rg_reset_user_model( id );

    set_entvar( id, var_viewmodel, "models/v_knife.mdl" );
    set_entvar( id, var_weaponmodel, "models/p_knife.mdl" );

    rg_remove_all_items( id );
    rg_give_item( id, "weapon_knife" );
    rg_give_item( id, "weapon_g3sg1" );
    rg_set_user_bpammo( id, WEAPON_G3SG1, 200 );

    if ( get_member_game( m_bGameStarted ) && !get_member_game( m_bFreezePeriod ) )
        RequestFrame( "@RequestFrame_CheckChangeClassWinConditions" );
}

@RequestFrame_CheckChangeClassWinConditions( )
{
    if ( get_member_game( m_iRoundWinStatus ) != WINSTATUS_NONE )
        return;

    if ( !get_member_game( m_bGameStarted ) )
        return;

    new aliveZombies = get_alive_zombies( );
    new aliveHumans = get_alive_humans( );

    if ( aliveZombies <= 0 )
    {
        stop_zombie_respawn( );

        rg_round_end( 3.0, WINSTATUS_CTS, ROUND_CTS_WIN, .trigger = true );
        return;
    }

    if ( aliveHumans <= 0 )
    {
        stop_zombie_respawn( );

        rg_round_end( 3.0, WINSTATUS_TERRORISTS, ROUND_TERRORISTS_WIN, .trigger = true );
        return;
    }

    new alive_t, dead_t,
        alive_ct, dead_ct;
    
    rg_initialize_player_counts( alive_t, alive_ct, dead_t, dead_ct );
    if ( alive_t + alive_ct + dead_t + dead_ct >= 2 )
    {
        if ( !alive_t )
            rg_round_end( 3.0, WINSTATUS_CTS, ROUND_CTS_WIN, .trigger = true );
        else if ( !alive_ct )
            rg_round_end( 3.0, WINSTATUS_TERRORISTS, ROUND_TERRORISTS_WIN, .trigger = true );
    }
}

@RequestFrame_ApplyPlayerAttributes( id )
{
    if ( BIT_VALID( m_bitZombie, BIT_PLAYER( id ) ) )
        @ZombieAttributes( id, 0 );
    else
        @HuamnAttributes( id, 0 );
}

@RequestFrame_TryJoinAsZombie( id )
{
    if ( !is_user_connected( id ) )
        return;

    if ( get_member_game( m_bFreezePeriod ) )
    {
        RequestFrame( "@RequestFrame_TryJoinAsZombie", id );
        return;
    }

    if ( !can_join_as_zombie( ) )
        return;

    BIT_ADD( m_bitJoinAsZombie, BIT_PLAYER( id ) );
    rg_set_user_team( id, TEAM_TERRORIST );

    if ( !is_user_alive( id ) )
        rg_round_respawn( id );
}

@Select_ZombieSpawnPoint( id )
{
    new numPoints = ArraySize( m_arrSpawnPoints );
    if ( !numPoints )
        return 0;

    new bestSpot = 0, safeSpot = 0;
    new Float: safeDistance = -1.0;

    new farthestSpot = 0;
    new Float: farthestDistance = -1.0;

    new Float: spawnOrigin[ 3 ];
    new Float: playerOrigin[ 3 ];
    new Float: minRequiredDistance = get_pcvar_float( m_pCvar_ZombieRespawnMinDistance ) * get_pcvar_float( m_pCvar_ZombieRespawnMinDistance );
    for ( new i = 0; i < numPoints; i++ )
    {
        new spot = ArrayGetCell( m_arrSpawnPoints, i );
        if ( is_nullent( spot ) )
            continue;

        get_entvar( spot, var_origin, spawnOrigin );
        if ( !IsHullVacant( id, spawnOrigin, HULL_HUMAN ) )
            continue;

        new Float: nearestHumanDistance = 999999999.0;
        new bool: hasHuman;
        for ( new j = 1; j <= MaxClients; j++ )
        {
            if ( !is_user_alive( j ) )
                continue;

            if ( is_player_afk( j ) )
                continue;

            if ( BIT_VALID( m_bitZombie, BIT_PLAYER( j ) ) )
                continue;

            hasHuman = true;
            get_entvar( j, var_origin, playerOrigin );

            new Float: distance = get_distance_squared( spawnOrigin, playerOrigin );
            if ( distance < nearestHumanDistance )
                nearestHumanDistance = distance;
        }

        if ( !hasHuman )
            nearestHumanDistance = 999999999.0;

        if ( nearestHumanDistance > farthestDistance )
        {
            farthestDistance = nearestHumanDistance;
            farthestSpot = spot;
        }

        if ( nearestHumanDistance < minRequiredDistance )
            continue;

        if ( nearestHumanDistance > safeDistance )
        {
            safeDistance = nearestHumanDistance;
            safeSpot = spot;
        }
    }

    if ( !is_nullent( safeSpot ) )
        return safeSpot;

    if ( !is_nullent( farthestSpot ) )
        return farthestSpot;

    bestSpot = @Select_SpawnPoint( id );
    return bestSpot;
}

@Select_SpawnPoint( id )
{
    new spot, lastSpot = m_iLastSpawn, numPoints = ArraySize( m_arrSpawnPoints ), Float: vecOrigin[ 3 ];
    do
    {
        if ( ++lastSpot >= numPoints )
            lastSpot = 0;

        spot = ArrayGetCell( m_arrSpawnPoints, lastSpot );
        if ( is_nullent( spot ) )
            continue;

        get_entvar( spot, var_origin, vecOrigin );
        if ( !IsHullVacant( id, vecOrigin, HULL_HUMAN ) )
            continue;

        break;
    }
    while ( lastSpot != m_iLastSpawn );

    if ( is_nullent( spot ) )
        return 0;

    m_iLastSpawn = lastSpot;
    return spot;
}

@Forcing_LevelInitialize( )
{
    if ( !ArraySize( m_arrSpawnPoints ) )
    {
        new entity = NULLENT;
        while ( ( entity = rg_find_ent_by_class( entity, "info_player_start", true ) ) )
            ArrayPushCell( m_arrSpawnPoints, entity );

        while ( ( entity = rg_find_ent_by_class( entity, "info_player_deathmatch", true ) ) )
            ArrayPushCell( m_arrSpawnPoints, entity );
    }

    new numPoints = ArraySize( m_arrSpawnPoints );
    set_member_game( m_iSpawnPointCount_Terrorist, numPoints );
    set_member_game( m_iSpawnPointCount_CT, numPoints );

    set_member_game( m_bLevelInitialized, true );
}

static stock Float: get_distance_squared( const Float: vecOrigin1[ 3 ], const Float: vecOrigin2[ 3 ] )
{
    new Float: x = vecOrigin1[ 0 ] - vecOrigin2[ 0 ];
    new Float: y = vecOrigin1[ 1 ] - vecOrigin2[ 1 ];
    new Float: z = vecOrigin1[ 2 ] - vecOrigin2[ 2 ];
    return x * x + y * y + z * z;
}

static stock bool: IsHullVacant( id, Float: vecOrigin[ 3 ], hull )
{
	engfunc( EngFunc_TraceHull, vecOrigin, vecOrigin, 0, hull, id, 0 );

	if ( get_tr2( 0, TR_StartSolid ) || get_tr2( 0, TR_AllSolid ) || !get_tr2( 0, TR_InOpen ) )
		return false;

	return true;
}

static stock bool: CanZombieRespawn( id )
{
    new alive = get_alive_players( );
    new zombies = get_alive_zombies( );
    new humans = get_alive_humans( );

    alive++;
    if ( alive <= 1 )
        return false;

    if ( humans <= 0 )
        return false;

    if ( humans == 1 )
        return false;

    if ( zombies >= humans )
        return false;

    new maxZombies = floatround( float( alive ) * get_max_zombie_count( ), floatround_ceil );
    if ( zombies + 1 > maxZombies )
        return false;

    if ( m_iZombieRespawn[ id ] >= get_pcvar_num( m_pCvar_ZombieRespawnLimit ) )
        return false;

    new respawnNumber = m_iZombieRespawn[ id ];
    new Float: chance = 100.0 - float( respawnNumber ) * get_pcvar_float( m_pCvar_ZombieRespawnChanceDecay );

    if ( chance <= 0.0 )
        return false;

    if ( random_float( 0.0, 100.0 ) > chance )
        return false;

    return true;
}

static stock bool: can_join_as_zombie( )
{
    if ( !get_member_game( m_bGameStarted ) )
        return false;

    if ( get_member_game( m_bRoundTerminating ) )
        return false;

    if ( !m_bZombieRespawnAllowed )
        return false;

    new alive = get_alive_players( );
    new zombies = get_alive_zombies( );
    new humans = get_alive_humans( );

    if ( humans <= 1 )
        return false;

    if ( zombies >= humans )
        return false;

    alive++;
    new maxZombies = floatround( float( alive ) * float( get_zombie_player_ratio( ) ) / 100.0, floatround_ceil );
    if ( zombies + 1 > maxZombies )
        return false;

    return true;
}

static stock bool: is_player_afk( id )
{
    if ( !is_user_connected( id ) )
        return true;

    new Float: lastMovement = get_member( id, m_fLastMovement );
    if ( lastMovement <= 0.0 )
        return false;

    return ( get_gametime( ) - lastMovement ) >= get_pcvar_float( m_pCvar_ZombieBalanceAfkTime );
}

static stock get_alive_players( )
{
    new count;
    for ( new i = 1; i <= MaxClients; i++ )
    {
        if ( !is_user_alive( i ) )
            continue;

        if ( is_player_afk( i ) )
            continue;

        count++;
    }

    return count;
}

static stock get_alive_zombies( )
{
    new count;
    for ( new i = 1; i <= MaxClients; i++ )
    {
        if ( !is_user_alive( i ) )
            continue;

        if ( is_player_afk( i ) )
            continue;

        if ( !BIT_VALID( m_bitZombie, BIT_PLAYER( i ) ) )
            continue;

        count++;
    }

    return count;
}

static stock get_alive_humans( )
{
    new count;
    for ( new i = 1; i <= MaxClients; i++ )
    {
        if ( !is_user_alive( i ) )
            continue;

        if ( is_player_afk( i ) )
            continue;

        if ( BIT_VALID( m_bitZombie, BIT_PLAYER( i ) ) )
            continue;

        count++;
    }

    return count;
}

static stock get_zombie_player_ratio( )
{
    new activePlayers = get_alive_players( );
    if ( activePlayers <= 4 )
        return 33;

    if ( activePlayers <= 9 )
        return 40;

    if ( activePlayers <= 19 )
        return 45;

    return 50;
}

static stock get_zombie_time_ratio( )
{
    new Float: roundStart = get_member_game( m_fRoundStartTime );
    if ( roundStart <= 0.0 )
        return 50;

    new Float: roundTime = get_gametime( ) - roundStart;
    if ( roundTime < 30.0 )
        return 50;

    if ( roundTime < 60.0 )
        return 45;

    if ( roundTime < 120.0 )
        return 40;

    return 35;
}

static stock get_max_zombie_count( )
{
    new activePlayers = get_alive_players( );
    if ( activePlayers <= 0 )
        return 0;

    new playerRatio = get_zombie_player_ratio( );
    new timeRatio = get_zombie_time_ratio( );
    new ratio = playerRatio;

    if ( timeRatio < ratio )
        ratio = timeRatio;

    new maxZombies = floatround( float( activePlayers ) * float( ratio ) / 100.0, floatround_ceil );
    if ( maxZombies < 1 )
        maxZombies = 1;

    return maxZombies;
}

static stock stop_zombie_respawn( )
{
    m_bZombieRespawnAllowed = false;

    for ( new i = 1; i <= MaxClients; i++ )
    {
        if ( !is_user_connected( i ) )
            continue;

        set_member( i, m_flRespawnPending, 0.0 );
        BIT_SUB( m_bitZombieRespawn, BIT_PLAYER( i ) );
    }
}

@UTIL_DeathMsg( killer, victim, headshot, const weapon[ ] )
{
    message_begin( MSG_ALL, get_user_msgid( "DeathMsg" ) );
    write_byte( killer );
    write_byte( victim );
    write_byte( headshot );
    write_string( weapon );
    message_end( );
}

@UTIL_ScoreAttrib( id, flags )
{
    message_begin( MSG_ALL, get_user_msgid( "ScoreAttrib" ) );
    write_byte( id );
    write_byte( flags );
    message_end( );
}

static stock bool: Read_JsonField( const fileName[ ] )
{
    new cfgsDir[ PLATFORM_MAX_PATH ];
    get_localinfo( "amxx_configsdir", cfgsDir, charsmax( cfgsDir ) );

    add( cfgsDir, charsmax( cfgsDir ), "/biohazard" );
    if ( !dir_exists( cfgsDir ) )
    {
        set_fail_state( "Directory '%s' not found.", cfgsDir );
        return false;
    }

    strcat( cfgsDir, fmt( "/%s.json", fileName ), charsmax( cfgsDir ) );
    if ( !file_exists( cfgsDir ) )
    {
        set_fail_state( "File '%s' not found.", cfgsDir );
        return false;
    }

    new JSON: jsonParse = json_parse( cfgsDir, true );
    if ( jsonParse == Invalid_JSON )
    {
        set_fail_state( "Parsing file '%s' error." );
        return false;
    }

    new JSON: jsonValue = json_object_get_value( jsonParse, "useless_entities" );
    if ( json_is_array( jsonValue ) )
    {
        m_trUselessEntities = TrieCreate( );

        new temp[ 32 ];
        for ( new i = 0; i < json_array_get_count( jsonValue ); i++ )
        {
            json_array_get_string( jsonValue, i, temp, charsmax( temp ) );
            TrieSetCell( m_trUselessEntities, temp, 1 );
        }

        json_free( jsonValue );

        if ( TrieGetSize( m_trUselessEntities ) )
            m_fwdSpawn = register_forward( FM_Spawn, "@FM_Spawn_Pre" );
        else
        {
            TrieDestroy( m_trUselessEntities );
            m_trUselessEntities = Invalid_Trie;
        }
    }

    new JSON: jsonClasses = json_object_get_value( jsonParse, "classes" );
    if ( json_is_array( jsonClasses ) )
    {
        new tempClass[ ClassData ], JSON: jsonItem;
        for ( new i = 0; i < json_array_get_count( jsonClasses ); i++ )
        {
            jsonItem = json_array_get_value( jsonClasses, i );
            json_object_get_string( jsonItem, "name", tempClass[ ClassName ], charsmax( tempClass[ ClassName ] ) );
            tempClass[ ClassHealth ] = _: json_object_get_real( jsonItem, "health" );
            tempClass[ ClassSpeed ] = _: json_object_get_real( jsonItem, "speed" );
            tempClass[ ClassGravity ] = _: json_object_get_real( jsonItem, "gravity" );
            json_object_get_string( jsonItem, "model", tempClass[ ClassModel ], charsmax( tempClass[ ClassModel ] ) );
            precache_model( fmt( "models/player/%s/%s.mdl", tempClass[ ClassModel ], tempClass[ ClassModel ] ) );
            json_object_get_string( jsonItem, "claw", tempClass[ ClassClaw ], charsmax( tempClass[ ClassClaw ] ) );
            precache_model( tempClass[ ClassClaw ] );

            ArrayPushArray( m_arrClassesVec, tempClass );
            json_free( jsonItem );
        }

        m_iNumClasses = ArraySize( m_arrClassesVec );
        json_free( jsonClasses );
    }

    json_free( jsonParse );
    return true;
}

@native_get_user_money( amxx, argc )
{
    enum { arg_id = 1 };

    new id = get_param( arg_id );
    if ( !is_user_connected( id ) )
    {
        log_error( AMX_ERR_NATIVE, "Invalid index '%d'.", id );
        return -1;
    }

    return m_iMoney[ id ];
}

@native_set_user_money( amxx, argc )
{
    enum { arg_id = 1, arg_amount, arg_flash };

    new id = get_param( arg_id );
    if ( !is_user_connected( id ) )
    {
        log_error( AMX_ERR_NATIVE, "Invalid index '%d'.", id );
        return 0;
    }

    new amount = get_param( arg_amount );
    if ( amount < 0 )
        amount = 0;
    else if ( amount > 12000 )
        amount = 12000;

    message_begin( MSG_ONE, get_user_msgid( "Money" ), .player = id );
    write_long( amount );
    write_byte( get_param( arg_flash ) );
    message_end( );

    set_member( id, m_iAccount, amount );
    m_iMoney[ id ] = amount;
    return 1;
}