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


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

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

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
{
    TASKID_WARMUP
};

enum _:ClassData
{
    ClassName[32],
    Float:ClassHealth,
    Float:ClassSpeed,
    Float:ClassGravity,
    ClassModel[64],
    ClassClaw[64]
};

new Array:m_arrClassesVec = Invalid_Array;

enum _:SpawnData
{
    Float:SpawnOrigin[3],
    Float:SpawnAngles[3]
};

new Array:m_arrSpawnPointsVec = Invalid_Array;
new Array:m_arrDefaultSpawnPointsVec = Invalid_Array;

new m_szMapName[64];
new m_szSpawnFile[PLATFORM_MAX_PATH];

new m_iNumClasses;
new m_iNumSpawnPoints;
new m_iMenuPosition[MAX_CLIENTS + 1];
new m_iLastSpawn;

new Trie:m_trUselessEntities = Invalid_Trie;
new Trie:m_trTextMessage = Invalid_Trie;
new Trie:m_trSendAudio = Invalid_Trie;

new bool:m_bWarmup = true;
new bool:m_bZombieRespawnAllowed;

new m_pCvar_FreezeTime;
new m_pCvar_RoundTime;
new m_pCvar_OverHumansWin;
new m_pCvar_ZombieRespawnTime;
new m_pCvar_ZombieRespawnMinDistance;
new m_pCvar_ZombieRespawnLimit;
new m_pCvar_ZombieRespawnChanceDecay;
new m_pCvar_MoneyAsJoin;
new m_pCvar_MoneyDamageRatio;
new m_pCvar_MoneyInfection;
new m_pCvar_MoneyKill;
new m_pCvar_ZombieRegenAmount;
new m_pCvar_ZombieRegenInterval;
new m_pCvar_ZombieRegenDelay;

new m_fwdSpawn;

new m_varClass[MAX_CLIENTS + 1];
new m_iZombieRespawn[MAX_CLIENTS + 1];
new m_iMoney[MAX_CLIENTS + 1];

new Float:m_flZombieRegenTime[MAX_CLIENTS + 1];
new Float:m_flZombieLastDamage[MAX_CLIENTS + 1];

new m_bitZombie;
new m_bitZombieRespawn;


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

    m_arrClassesVec = ArrayCreate(ClassData);

    m_trTextMessage = TrieCreate();

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

    m_trSendAudio = TrieCreate();

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

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

    for (new i; 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 to read biohazard config.");
}


public plugin_init()
{
    m_pCvar_OverHumansWin =
        register_cvar("bio_over_humans_win", "1");

    m_pCvar_ZombieRespawnTime =
        register_cvar("bio_zombie_respawn", "3.0");

    m_pCvar_ZombieRespawnMinDistance =
        register_cvar("bio_zombie_respawn_min_distance", "1200.0");

    m_pCvar_ZombieRespawnLimit =
        register_cvar("bio_zombie_respawn_limit", "3");

    m_pCvar_ZombieRespawnChanceDecay =
        register_cvar("bio_zombie_chance_decay", "25.0");

    m_pCvar_MoneyAsJoin =
        register_cvar("bio_money_join", "350");

    m_pCvar_MoneyDamageRatio =
        register_cvar("bio_money_damage_ratio", "0.26");

    m_pCvar_MoneyInfection =
        register_cvar("bio_money_infection", "100");

    m_pCvar_MoneyKill =
        register_cvar("bio_money_kill", "100");

    m_pCvar_ZombieRegenAmount =
        register_cvar("bio_zombie_regen_amount", "1.0");

    m_pCvar_ZombieRegenInterval =
        register_cvar("bio_zombie_regen_interval", "0.15");

    m_pCvar_ZombieRegenDelay =
        register_cvar("bio_zombie_regen_delay", "2.48");

    register_clcmd("say /class", "@Open_ClassesMenu");
    register_clcmd("say_team /class", "@Open_ClassesMenu");

    register_clcmd("infect", "cmd_infect");
    register_clcmd("antidote", "cmd_antidote");

    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_AddAccount,
        "@CBasePlayer_AddAccount_Pre"
    );

    RegisterHookChain(
        RG_CBasePlayer_TakeDamage,
        "@CBasePlayer_TakeDamage_Pre"
    );

    RegisterHookChain(
        RG_CBasePlayer_ResetMaxSpeed,
        "@CBasePlayer_ResetMaxSpeed_Pre"
    );

    RegisterHookChain(
        RG_CBasePlayer_Killed,
        "@CBasePlayer_Killed_Post",
        true
    );

    RegisterHookChain(
        RG_CBasePlayer_PreThink,
        "@CBasePlayer_PreThink_Pre"
    );
}


public plugin_cfg()
{
    m_arrSpawnPointsVec = ArrayCreate(SpawnData);
    m_arrDefaultSpawnPointsVec = ArrayCreate(1);

    get_mapname(
        m_szMapName,
        charsmax(m_szMapName)
    );

    get_localinfo(
        "amxx_configsdir",
        m_szSpawnFile,
        charsmax(m_szSpawnFile)
    );

    add(
        m_szSpawnFile,
        charsmax(m_szSpawnFile),
        "/biohazard/spawns.json"
    );

    /*
     * Загружаем стандартные spawn entity.
     */
    @Load_DefaultSpawnPoints();

    /*
     * Загружаем custom spawn points
     * только для текущей карты.
     */
    Load_SpawnPoints();

    /*
     * После загрузки spawn points
     * обновляем количество spawn points
     * в GameRules.
     */
    @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);

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

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

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

    if (m_arrSpawnPointsVec != Invalid_Array)
        ArrayDestroy(m_arrSpawnPointsVec);

    if (m_arrDefaultSpawnPointsVec != Invalid_Array)
        ArrayDestroy(m_arrDefaultSpawnPointsVec);
}


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


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

    m_flZombieRegenTime[id] = 0.0;
    m_flZombieLastDamage[id] = 0.0;

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


public cmd_infect(id)
{
    @ZombieAttributes(id, 0);
}


public cmd_antidote(id)
{
    @HumanAttributes(id);
}


@Open_ClassesMenu(id)
{
    if (!m_iNumClasses)
    {
        client_print(
            id,
            print_chat,
            "Zombie classes not found!"
        );

        return PLUGIN_HANDLED;
    }

    m_iMenuPosition[id] = 0;

    return @Display_ClassesMenu(id, 0);
}


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

    new start = pos * 7;

    if (start > m_iNumClasses)
        start = m_iNumClasses;

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

    new end = start + 7;

    if (end > m_iNumClasses)
        end = m_iNumClasses;

    new page =
        (m_iNumClasses / 7) +
        (m_iNumClasses % 7 ? 1 : 0);

    new key = 1 << 9;

    new menu[512];
    new data[ClassData];

    new len = formatex(
        menu,
        charsmax(menu),
        "\yClasses %d/%d^n^n",
        pos + 1,
        page
    );

    new item;

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

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

        len += formatex(
            menu[len],
            charsmax(menu) - len,
            "\y%d. %s%s^n",
            ++item,
            m_varClass[id] == start ? "\d" : "\w",
            data[ClassName]
        );

        start++;
    }

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

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

    len += formatex(
        menu[len],
        charsmax(menu) - len,
        "^n\y8. %sBack",
        pos ? "\w" : "\d"
    );

    len += formatex(
        menu[len],
        charsmax(menu) - len,
        "^n\y9. %sNext",
        end < m_iNumClasses ? "\w" : "\d"
    );

    formatex(
        menu[len],
        charsmax(menu) - len,
        "^n\y0. \wExit"
    );

    return show_menu(
        id,
        key,
        menu,
        -1,
        "@Display_ClassesMenu"
    );
}


@Handle_ClassesMenu(id, key)
{
    switch (key)
    {
        case 7:
            return @Display_ClassesMenu(
                id,
                --m_iMenuPosition[id]
            );

        case 8:
            return @Display_ClassesMenu(
                id,
                ++m_iMenuPosition[id]
            );

        case 9:
            return PLUGIN_HANDLED;
    }

    new class =
        m_iMenuPosition[id] * 7 +
        key;

    if (class >= m_iNumClasses)
        return PLUGIN_HANDLED;

    m_varClass[id] = class;

    new data[ClassData];

    ArrayGetArray(
        m_arrClassesVec,
        class,
        data
    );

    client_print(
        id,
        print_chat,
        "You have selected the %s class.",
        data[ClassName]
    );

    return PLUGIN_CONTINUE;
}


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

        if (!equal(
            new_value,
            IMMUTABLE_CVARS[i][1]
        ))
        {
            set_pcvar_string(
                pcvar,
                IMMUTABLE_CVARS[i][1]
            );
        }

        break;
    }
}


@Message_TextMsg(msg_id, msg_dest, receiver)
{
    return @BlockMessage(m_trTextMessage);
}


@Message_SendAudio(msg_id, msg_dest, receiver)
{
    return @BlockMessage(m_trSendAudio);
}


@BlockMessage(Trie:trie)
{
    new text[32];
    new value;

    get_msg_arg_string(
        2,
        text,
        charsmax(text)
    );

    return TrieGetCell(
        trie,
        text,
        value
    )
        ? PLUGIN_HANDLED
        : PLUGIN_CONTINUE;
}


@Message_Money(msg_id, msg_dest, receiver)
{
    if (!is_user_connected(receiver))
        return PLUGIN_CONTINUE;

    set_msg_arg_int(
        1,
        get_msg_argtype(1),
        m_iMoney[receiver]
    );

    return PLUGIN_CONTINUE;
}


@Task_Warmup(task)
{
    if (get_member_game(m_bRoundTerminating))
        return;

    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)
        return;

    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 &&
        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;

    new TeamName:winTeam = TEAM_UNASSIGNED;
    new TeamName:team;

    switch (status)
    {
        case WINSTATUS_CTS:
        {
            client_print(
                0,
                print_center,
                "Humans won!"
            );

            client_cmd(
                0,
                "spk ^"ambience/xtal_down1.wav^""
            );

            winTeam = TEAM_CT;
        }

        case WINSTATUS_TERRORISTS:
        {
            client_print(
                0,
                print_center,
                "Zombies won!"
            );

            client_cmd(
                0,
                "spk ^"ambience/the_horror1.wav^""
            );

            winTeam = TEAM_TERRORIST;
        }

        case WINSTATUS_DRAW:
        {
            client_print(
                0,
                print_center,
                "No one won!"
            );

            client_cmd(
                0,
                "spk ^"ambience/3dmstart.wav^""
            );
        }
    }

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

        team = get_member(i, m_iTeam);

        if (
            team != TEAM_CT &&
            team != TEAM_TERRORIST
        )
        {
            continue;
        }

        if (winTeam == team)
        {
            new reward = random_num(100, 500);

            bio_set_user_money(
                i,
                bio_get_user_money(i) + reward
            );

            client_print(
                i,
                print_chat,
                "Won %s!",
                team == TEAM_CT
                    ? "humans"
                    : "zombies"
            );

            client_print(
                i,
                print_chat,
                "Reward %d.",
                reward
            );
        }
    }
}


@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();

    m_bitZombie = 0;

    for (new i = 1; i <= MaxClients; i++)
    {
        if (is_user_connected(i))
            m_iZombieRespawn[i] = 0;
    }

    @Forcing_LevelInitialize();
}


@CSGameRules_CheckWinConditions_Pre()
{
    if (
        m_bWarmup ||
        !get_member_game(m_bGameStarted) ||
        get_member_game(m_bRoundTerminating)
    )
    {
        return HC_SUPERCEDE;
    }

    return HC_SUPERCEDE;
}


@RequestFrame_CheckChangeClassWinConditions()
{
    if (
        get_member_game(m_iRoundWinStatus) != WINSTATUS_NONE ||
        !get_member_game(m_bGameStarted) ||
        get_member_game(m_bRoundTerminating)
    )
    {
        return;
    }

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

    if (!zombies)
    {
        stop_zombie_respawn();

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

        return;
    }

    if (!humans)
    {
        stop_zombie_respawn();

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


@CSGameRules_FPlayerCanTakeDamage_Pre(
    id,
    attacker
)
{
    if (
        get_member_game(m_bRoundTerminating) ||
        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 players[MAX_CLIENTS];
    new count;

    for (new i = 1; i <= MaxClients; i++)
    {
        if (is_user_alive(i))
            players[count++] = i;
    }

    if (count < 2)
        return;

    new need =
        floatround(
            float(count) * 0.20,
            floatround_ceil
        );

    while (need--)
    {
        new pos = random(count);
        new id = players[pos];

        players[pos] = players[--count];

        @ZombieAttributes(id, 0);
    }
}


/*
 * ============================================================
 * PLAYER SPAWN
 * ============================================================
 *
 * Если custom points существуют:
 *
 *     custom spawn
 *
 * Если custom points отсутствуют:
 *
 *     вообще не вмешиваемся в ReGameDLL.
 *     Он использует стандартные:
 *
 *     info_player_start
 *     info_player_deathmatch
 *
 * Если custom point существует, но выбрать его невозможно:
 *
 *     fallback -> стандартный spawn.
 *
 * ============================================================
 */

@CSGameRules_GetPlayerSpawnSpot_Pre(id)
{
    if (!IsPlaying(id))
        return HC_CONTINUE;

    /*
     * CUSTOM SPAWN НЕ НАЙДЕН.
     *
     * Полностью отдаём управление ReGameDLL.
     */
    if (!is_spawn_points())
        return HC_CONTINUE;

    new index;

    if (BIT_VALID(m_bitZombie, id))
        index = @Select_ZombieSpawnPoint(id);
    else
        index = @Select_SpawnPoint(id);

    /*
     * Не удалось подобрать custom point.
     *
     * Используем стандартную spawn entity.
     */
    if (index < 0)
        return HC_CONTINUE;

    new data[SpawnData];

    if (!get_spawn_point(index, data))
        return HC_CONTINUE;

    new Float:origin[3];

    origin = data[SpawnOrigin];

    origin[2] += 1.0;

    set_entvar(
        id,
        var_origin,
        origin
    );

    set_entvar(
        id,
        var_v_angle,
        data[SpawnAngles]
    );

    set_entvar(
        id,
        var_velocity,
        NULL_VECTOR
    );

    set_entvar(
        id,
        var_angles,
        data[SpawnAngles]
    );

    set_entvar(
        id,
        var_punchangle,
        NULL_VECTOR
    );

    set_entvar(
        id,
        var_fixangle,
        1
    );

    /*
     * GetPlayerSpawnSpot() должен вернуть entity.
     *
     * Для custom spawn нам не нужен реальный
     * spawn entity как источник координат.
     *
     * Используем стандартный spawn entity только
     * как технический return value.
     */
    new spot = @Get_DefaultSpawnEntity(id);

    if (is_nullent(spot))
    {
        /*
         * На карте вообще нет стандартных spawn entity.
         *
         * Возвращаем самого игрока как последний fallback.
         */
        spot = id;
    }

    SetHookChainReturn(
        ATYPE_INTEGER,
        spot
    );

    return HC_SUPERCEDE;
}


@CBasePlayer_Spawn_Pre(id)
{
    if (
        get_member(id, m_bJustConnected) ||
        !IsPlaying(id)
    )
    {
        return;
    }

    RequestFrame(
        "@RequestFrame_ApplyPlayerAttributes",
        id
    );
}


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

        return;
    }

    if (!IsPlaying(id))
        return;

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

    if (BIT_VALID(m_bitZombieRespawn, id))
    {
        BIT_SUB(
            m_bitZombieRespawn,
            id
        );

        set_member(
            id,
            m_flRespawnPending,
            0.0
        );
    }
}


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

    SetHookChainReturn(
        ATYPE_BOOL,
        true
    );

    return HC_SUPERCEDE;
}


@CBasePlayer_AddAccount_Pre(
    id,
    amount,
    RewardType:type,
    bool:trackChange
)
{
    if (type == RT_NONE)
        return HC_CONTINUE;

    if (type == RT_PLAYER_JOIN)
    {
        new joinMoney =
            get_pcvar_num(
                m_pCvar_MoneyAsJoin
            );

        m_iMoney[id] = joinMoney;

        SetHookChainArg(
            2,
            ATYPE_INTEGER,
            joinMoney
        );

        return HC_CONTINUE;
    }

    return HC_SUPERCEDE;
}


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

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

    if (BIT_VALID(m_bitZombie, victim))
    {
        new Float:gametime =
            get_gametime();

        m_flZombieLastDamage[victim] =
            gametime;

        m_flZombieRegenTime[victim] =
            gametime +
            get_pcvar_float(
                m_pCvar_ZombieRegenInterval
            );
    }

    if (
        victim == attacker ||
        !is_user_connected(attacker)
    )
    {
        return;
    }

    if (BIT_VALID(m_bitZombie, attacker))
    {
        new weapon =
            get_member(
                attacker,
                m_pActiveItem
            );

        if (
            !is_nullent(weapon) &&
            get_member(
                weapon,
                m_iId
            ) == WEAPON_KNIFE &&
            inflictor == attacker &&
            !(damage_type & (
                DMG_GRENADE |
                DMG_BLAST
            ))
        )
        {
            if (get_alive_humans() <= 1)
                return;

            @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 money =
        floatround(
            damage *
            get_pcvar_float(
                m_pCvar_MoneyDamageRatio
            ),
            floatround_floor
        );

    bio_set_user_money(
        attacker,
        bio_get_user_money(attacker) +
        money
    );
}


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

    if (BIT_VALID(m_bitZombie, id))
    {
        new data[ClassData];

        ArrayGetArray(
            m_arrClassesVec,
            m_varClass[id],
            data
        );

        set_entvar(
            id,
            var_maxspeed,
            data[ClassSpeed]
        );

        return HC_SUPERCEDE;
    }

    new weapon =
        get_member(
            id,
            m_pActiveItem
        );

    new Float:speed;

    if (!is_nullent(weapon))
    {
        ExecuteHamB(
            Ham_CS_Item_GetMaxSpeed,
            weapon,
            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_bGameStarted) ||
        get_member_game(m_bRoundTerminating)
    )
    {
        return;
    }

    if (is_user_connected(attacker))
    {
        bio_set_user_money(
            attacker,
            bio_get_user_money(attacker) +
            get_pcvar_num(
                m_pCvar_MoneyKill
            )
        );
    }

    if (
        BIT_VALID(m_bitZombie, id) &&
        m_bZombieRespawnAllowed &&
        can_zombie_respawn(id)
    )
    {
        new Float:time =
            get_pcvar_float(
                m_pCvar_ZombieRespawnTime
            );

        if (time > 0.0)
        {
            BIT_ADD(
                m_bitZombieRespawn,
                id
            );

            m_iZombieRespawn[id]++;

            set_member(
                id,
                m_flRespawnPending,
                get_gametime() + time
            );
        }
    }

    RequestFrame(
        "@RequestFrame_CheckChangeClassWinConditions"
    );
}


@CBasePlayer_PreThink_Pre(id)
{
    if (!is_user_alive(id))
        return;

    if (get_member_game(m_bRoundTerminating))
        return;

    if (!BIT_VALID(m_bitZombie, id))
        return;

    new data[ClassData];

    ArrayGetArray(
        m_arrClassesVec,
        m_varClass[id],
        data
    );

    new Float:health;

    get_entvar(
        id,
        var_health,
        health
    );

    new Float:gametime =
        get_gametime();

    if (
        gametime -
        m_flZombieLastDamage[id] <
        get_pcvar_float(
            m_pCvar_ZombieRegenDelay
        )
    )
    {
        return;
    }

    if (
        gametime <
        m_flZombieRegenTime[id]
    )
    {
        return;
    }

    health +=
        get_pcvar_float(
            m_pCvar_ZombieRegenAmount
        );

    if (health > data[ClassHealth])
        health = data[ClassHealth];

    set_entvar(
        id,
        var_health,
        health
    );

    m_flZombieRegenTime[id] =
        gametime +
        get_pcvar_float(
            m_pCvar_ZombieRegenInterval
        );
}


@FM_Spawn_Pre(entity)
{
    new classname[32];

    get_entvar(
        entity,
        var_classname,
        classname,
        charsmax(classname)
    );

    if (!TrieKeyExists(
        m_trUselessEntities,
        classname
    ))
    {
        return FMRES_IGNORED;
    }

    forward_return(
        FMV_CELL,
        NULLENT
    );

    return FMRES_SUPERCEDE;
}


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

    m_flZombieRegenTime[id] =
        get_gametime();

    m_flZombieLastDamage[id] =
        get_gametime();

    new data[ClassData];

    ArrayGetArray(
        m_arrClassesVec,
        m_varClass[id],
        data
    );

    set_entvar(
        id,
        var_health,
        data[ClassHealth]
    );

    set_entvar(
        id,
        var_gravity,
        data[ClassGravity]
    );

    set_entvar(
        id,
        var_armorvalue,
        0.0
    );

    set_member(
        id,
        m_iKevlar,
        ARMOR_NONE
    );

    rg_set_user_model(
        id,
        data[ClassModel]
    );

    rg_remove_all_items(id);

    rg_give_item(
        id,
        "weapon_knife"
    );

    set_entvar(
        id,
        var_viewmodel,
        data[ClassClaw]
    );

    set_entvar(
        id,
        var_weaponmodel,
        NULL_STRING
    );

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

    @UTIL_DeathMsg(
        attacker ? attacker : id,
        id,
        0,
        "infection"
    );

    @UTIL_ScoreAttrib(
        id,
        0
    );

    rg_set_user_team(
        id,
        TEAM_TERRORIST
    );

    if (attacker)
    {
        ExecuteHamB(
            Ham_AddPoints,
            id,
            0,
            true
        );

        ExecuteHamB(
            Ham_AddPoints,
            attacker,
            1,
            true
        );

        set_member(
            id,
            m_iDeaths,
            get_member(
                id,
                m_iDeaths
            ) + 1
        );
    }

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


@HumanAttributes(id)
{
    rg_set_user_team(
        id,
        TEAM_CT
    );

    BIT_SUB(
        m_bitZombie,
        id
    );

    m_flZombieRegenTime[id] = 0.0;
    m_flZombieLastDamage[id] = 0.0;

    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"
    );

    give_human_equipment(id);

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


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

    if (BIT_VALID(m_bitZombie, id))
    {
        if (is_user_bot(id))
        {
            m_varClass[id] =
                random(m_iNumClasses);
        }

        @ZombieAttributes(
            id,
            0
        );
    }
    else
    {
        @HumanAttributes(id);
    }
}


/*
 * ============================================================
 * CUSTOM ZOMBIE SPAWN
 * ============================================================
 */

@Select_ZombieSpawnPoint(id)
{
    new numPoints =
        ArraySize(m_arrSpawnPointsVec);

    if (!numPoints)
        return -1;

    new safeIndex = -1;
    new farthestIndex = -1;

    new Float:safeDistance = -1.0;
    new Float:farthestDistance = -1.0;

    new Float:minDistance =
        get_pcvar_float(
            m_pCvar_ZombieRespawnMinDistance
        );

    minDistance *= minDistance;

    new Float:spawnOrigin[3];
    new Float:playerOrigin[3];

    for (new i = 0; i < numPoints; i++)
    {
        new data[SpawnData];

        if (!get_spawn_point(i, data))
            continue;

        spawnOrigin =
            data[SpawnOrigin];

        if (!IsHullVacant(
            id,
            spawnOrigin,
            HULL_HUMAN
        ))
        {
            continue;
        }

        new Float:nearest =
            999999999.0;

        for (
            new j = 1;
            j <= MaxClients;
            j++
        )
        {
            if (
                !is_user_alive(j) ||
                BIT_VALID(m_bitZombie, j)
            )
            {
                continue;
            }

            get_entvar(
                j,
                var_origin,
                playerOrigin
            );

            new Float:distance =
                get_distance_squared(
                    spawnOrigin,
                    playerOrigin
                );

            if (distance < nearest)
                nearest = distance;
        }

        if (nearest > farthestDistance)
        {
            farthestDistance =
                nearest;

            farthestIndex =
                i;
        }

        if (nearest < minDistance)
            continue;

        if (nearest > safeDistance)
        {
            safeDistance =
                nearest;

            safeIndex =
                i;
        }
    }

    if (safeIndex >= 0)
        return safeIndex;

    if (farthestIndex >= 0)
        return farthestIndex;

    /*
     * Custom точки существуют,
     * но все они заняты.
     *
     * Пытаемся использовать обычный
     * custom spawn.
     */
    return @Select_SpawnPoint(id);
}


/*
 * ============================================================
 * CUSTOM HUMAN SPAWN
 * ============================================================
 */

@Select_SpawnPoint(id)
{
    new numPoints =
        ArraySize(m_arrSpawnPointsVec);

    if (!numPoints)
        return -1;

    new lastSpot =
        m_iLastSpawn;

    new spot = -1;

    new Float:origin[3];

    do
    {
        if (++lastSpot >= numPoints)
            lastSpot = 0;

        new data[SpawnData];

        if (!get_spawn_point(
            lastSpot,
            data
        ))
        {
            continue;
        }

        origin =
            data[SpawnOrigin];

        if (!IsHullVacant(
            id,
            origin,
            HULL_HUMAN
        ))
        {
            continue;
        }

        spot = lastSpot;

        break;
    }
    while (lastSpot != m_iLastSpawn);

    if (spot < 0)
        return -1;

    m_iLastSpawn =
        spot;

    return spot;
}


/*
 * ============================================================
 * STANDARD SPAWN
 * ============================================================
 */

@Load_DefaultSpawnPoints()
{
    if (
        m_arrDefaultSpawnPointsVec ==
        Invalid_Array
    )
    {
        return;
    }

    ArrayClear(
        m_arrDefaultSpawnPointsVec
    );

    new entity = NULLENT;

    while (
        (
            entity =
            rg_find_ent_by_class(
                entity,
                "info_player_start",
                true
            )
        )
    )
    {
        ArrayPushCell(
            m_arrDefaultSpawnPointsVec,
            entity
        );
    }

    entity = NULLENT;

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

    server_print(
        "[Biohazard] Default spawn points: %d",
        ArraySize(
            m_arrDefaultSpawnPointsVec
        )
    );
}


static stock @Get_DefaultSpawnEntity(id)
{
    new count =
        ArraySize(
            m_arrDefaultSpawnPointsVec
        );

    if (!count)
        return NULLENT;

    /*
     * Сначала ищем свободный spawn.
     */
    new Float:origin[3];

    for (new i = 0; i < count; i++)
    {
        new spot =
            ArrayGetCell(
                m_arrDefaultSpawnPointsVec,
                i
            );

        if (is_nullent(spot))
            continue;

        get_entvar(
            spot,
            var_origin,
            origin
        );

        if (IsHullVacant(
            id,
            origin,
            HULL_HUMAN
        ))
        {
            return spot;
        }
    }

    /*
     * Если все заняты — просто
     * возвращаем первую валидную.
     */
    for (new i = 0; i < count; i++)
    {
        new spot =
            ArrayGetCell(
                m_arrDefaultSpawnPointsVec,
                i
            );

        if (!is_nullent(spot))
            return spot;
    }

    return NULLENT;
}


/*
 * ============================================================
 * LEVEL INITIALIZATION
 * ============================================================
 */

@Forcing_LevelInitialize()
{
    new count;

    /*
     * Есть custom spawn?
     */
    if (is_spawn_points())
    {
        count =
            ArraySize(
                m_arrSpawnPointsVec
            );

        server_print(
            "[Biohazard] Using %d custom spawn points on %s.",
            count,
            m_szMapName
        );
    }
    else
    {
        /*
         * Custom spawn нет.
         *
         * Используем стандартные.
         */
        count =
            ArraySize(
                m_arrDefaultSpawnPointsVec
            );

        server_print(
            "[Biohazard] No custom spawn points on %s. Using %d default spawn points.",
            m_szMapName,
            count
        );
    }

    set_member_game(
        m_iSpawnPointCount_Terrorist,
        count
    );

    set_member_game(
        m_iSpawnPointCount_CT,
        count
    );

    set_member_game(
        m_bLevelInitialized,
        true
    );
}


/*
 * ============================================================
 * SPAWN DATA
 * ============================================================
 */

static stock add_spawn_points(id)
{
    if (!is_user_connected(id))
        return;

    if (
        m_arrSpawnPointsVec ==
        Invalid_Array
    )
    {
        return;
    }

    new data[SpawnData];

    get_entvar(
        id,
        var_origin,
        data[SpawnOrigin]
    );

    get_entvar(
        id,
        var_v_angle,
        data[SpawnAngles]
    );

    data[SpawnAngles][0] = 0.0;
    data[SpawnAngles][2] = 0.0;

    ArrayPushArray(
        m_arrSpawnPointsVec,
        data
    );

    m_iNumSpawnPoints =
        ArraySize(
            m_arrSpawnPointsVec
        );
}


static stock find_nearest_spawn(id)
{
    if (!is_spawn_points())
        return -1;

    new Float:playerOrigin[3];

    get_entvar(
        id,
        var_origin,
        playerOrigin
    );

    new Float:bestDistance =
        999999999.0;

    new bestIndex = -1;

    new data[SpawnData];

    for (
        new i = 0;
        i < m_iNumSpawnPoints;
        i++
    )
    {
        if (!get_spawn_point(
            i,
            data
        ))
        {
            continue;
        }

        new Float:distance =
            get_distance_squared(
                playerOrigin,
                data[SpawnOrigin]
            );

        if (distance < bestDistance)
        {
            bestDistance =
                distance;

            bestIndex =
                i;
        }
    }

    return bestIndex;
}


static stock bool:del_spawn_points(index)
{
    if (
        index < 0 ||
        index >= m_iNumSpawnPoints
    )
    {
        return false;
    }

    ArrayDeleteItem(
        m_arrSpawnPointsVec,
        index
    );

    m_iNumSpawnPoints =
        ArraySize(
            m_arrSpawnPointsVec
        );

    if (
        m_iLastSpawn >=
        m_iNumSpawnPoints
    )
    {
        m_iLastSpawn = 0;
    }

    return true;
}


static stock bool:move_spawn_points(id, index)
{
    new data[SpawnData];

    if (!get_spawn_point(
        index,
        data
    ))
    {
        return false;
    }

    get_entvar(
        id,
        var_origin,
        data[SpawnOrigin]
    );

    ArraySetArray(
        m_arrSpawnPointsVec,
        index,
        data
    );

    return true;
}


static stock bool:rotate_spawn_points(id, index)
{
    new data[SpawnData];

    if (!get_spawn_point(
        index,
        data
    ))
    {
        return false;
    }

    new Float:vecAngles[3];

    get_entvar(
        id,
        var_v_angle,
        vecAngles
    );

    data[SpawnAngles][0] = 0.0;
    data[SpawnAngles][1] = vecAngles[1];
    data[SpawnAngles][2] = 0.0;

    ArraySetArray(
        m_arrSpawnPointsVec,
        index,
        data
    );

    return true;
}


static stock bool:teleport_to_spawn(id, index)
{
    new data[SpawnData];

    if (!get_spawn_point(
        index,
        data
    ))
    {
        return false;
    }

    set_entvar(
        id,
        var_origin,
        data[SpawnOrigin]
    );

    set_entvar(
        id,
        var_v_angle,
        data[SpawnAngles]
    );

    set_entvar(
        id,
        var_angles,
        data[SpawnAngles]
    );

    set_entvar(
        id,
        var_velocity,
        NULL_VECTOR
    );

    set_entvar(
        id,
        var_punchangle,
        NULL_VECTOR
    );

    set_entvar(
        id,
        var_fixangle,
        1
    );

    return true;
}


static stock del_all_spawn_points()
{
    if (
        m_arrSpawnPointsVec ==
        Invalid_Array
    )
    {
        return;
    }

    ArrayClear(
        m_arrSpawnPointsVec
    );

    m_iNumSpawnPoints = 0;
    m_iLastSpawn = 0;
}


static stock bool:get_spawn_point(
    index,
    data[SpawnData]
)
{
    if (
        m_arrSpawnPointsVec ==
        Invalid_Array
    )
    {
        return false;
    }

    if (
        index < 0 ||
        index >=
        ArraySize(
            m_arrSpawnPointsVec
        )
    )
    {
        return false;
    }

    ArrayGetArray(
        m_arrSpawnPointsVec,
        index,
        data
    );

    return true;
}


/*
 * ============================================================
 * SPAWN CHECKS
 * ============================================================
 */

static stock bool:is_spawn_points()
{
    return (
        m_arrSpawnPointsVec != Invalid_Array &&
        ArraySize(m_arrSpawnPointsVec) > 0
    );
}


static stock bool:is_default_spawn_points()
{
    return (
        m_arrDefaultSpawnPointsVec != Invalid_Array &&
        ArraySize(m_arrDefaultSpawnPointsVec) > 0
    );
}


static stock select_default_spawn_points(id)
{
    new count =
        ArraySize(
            m_arrDefaultSpawnPointsVec
        );

    if (!count)
        return NULLENT;

    return ArrayGetCell(
        m_arrDefaultSpawnPointsVec,
        random(count)
    );
}


static stock select_spawn_points(id)
{
    new count =
        ArraySize(
            m_arrSpawnPointsVec
        );

    if (!count)
        return -1;

    return random(count);
}


static stock Float:get_distance_squared(
    const Float:a[3],
    const Float:b[3]
)
{
    new Float:x =
        a[0] - b[0];

    new Float:y =
        a[1] - b[1];

    new Float:z =
        a[2] - b[2];

    return
        x * x +
        y * y +
        z * z;
}


/*
 * ============================================================
 * JSON CONFIG
 * ============================================================
 */

static stock bool:Read_JsonField(
    const fileName[]
)
{
    new path[PLATFORM_MAX_PATH];

    get_localinfo(
        "amxx_configsdir",
        path,
        charsmax(path)
    );

    add(
        path,
        charsmax(path),
        "/biohazard"
    );

    if (!dir_exists(path))
    {
        set_fail_state(
            "Directory '%s' not found.",
            path
        );

        return false;
    }

    formatex(
        path,
        charsmax(path),
        "%s/%s.json",
        path,
        fileName
    );

    if (!file_exists(path))
    {
        set_fail_state(
            "File '%s' not found.",
            path
        );

        return false;
    }

    new JSON:jsonParse =
        json_parse(
            path,
            true
        );

    if (jsonParse == Invalid_JSON)
    {
        set_fail_state(
            "Parsing file '%s' error.",
            path
        );

        return false;
    }

    new JSON:value =
        json_object_get_value(
            jsonParse,
            "useless_entities"
        );

    if (json_is_array(value))
    {
        m_trUselessEntities =
            TrieCreate();

        new classname[32];

        for (
            new i;
            i < json_array_get_count(value);
            i++
        )
        {
            json_array_get_string(
                value,
                i,
                classname,
                charsmax(classname)
            );

            TrieSetCell(
                m_trUselessEntities,
                classname,
                1
            );
        }

        json_free(value);

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

            m_trUselessEntities =
                Invalid_Trie;
        }
    }
    else if (value != Invalid_JSON)
    {
        json_free(value);
    }

    new JSON:classes =
        json_object_get_value(
            jsonParse,
            "classes"
        );

    if (json_is_array(classes))
    {
        new data[ClassData];
        new JSON:item;

        for (
            new i;
            i < json_array_get_count(classes);
            i++
        )
        {
            item =
                json_array_get_value(
                    classes,
                    i
                );

            if (!json_is_object(item))
            {
                if (item != Invalid_JSON)
                    json_free(item);

                continue;
            }

            json_object_get_string(
                item,
                "name",
                data[ClassName],
                charsmax(data[ClassName])
            );

            data[ClassHealth] =
                _:json_object_get_real(
                    item,
                    "health"
                );

            data[ClassSpeed] =
                _:json_object_get_real(
                    item,
                    "speed"
                );

            data[ClassGravity] =
                _:json_object_get_real(
                    item,
                    "gravity"
                );

            json_object_get_string(
                item,
                "model",
                data[ClassModel],
                charsmax(data[ClassModel])
            );

            precache_model(
                fmt(
                    "models/player/%s/%s.mdl",
                    data[ClassModel],
                    data[ClassModel]
                )
            );

            json_object_get_string(
                item,
                "claw",
                data[ClassClaw],
                charsmax(data[ClassClaw])
            );

            precache_model(
                data[ClassClaw]
            );

            ArrayPushArray(
                m_arrClassesVec,
                data
            );

            json_free(item);
        }

        m_iNumClasses =
            ArraySize(
                m_arrClassesVec
            );

        json_free(classes);
    }
    else if (classes != Invalid_JSON)
    {
        json_free(classes);
    }

    json_free(jsonParse);

    return true;
}


/*
 * ============================================================
 * LOAD CUSTOM SPAWN POINTS
 * ============================================================
 *
 * JSON:
 *
 * {
 *     "maps": {
 *         "de_dust2": {
 *             "spawns": [
 *                 {
 *                     "origin": [125.0, 300.0, 64.0],
 *                     "angles": [0.0, 90.0, 0.0]
 *                 }
 *             ]
 *         }
 *     }
 * }
 *
 * ============================================================
 */

static stock bool:Load_SpawnPoints()
{
    if (
        m_arrSpawnPointsVec ==
        Invalid_Array
    )
    {
        return false;
    }

    ArrayClear(
        m_arrSpawnPointsVec
    );

    m_iNumSpawnPoints = 0;
    m_iLastSpawn = 0;

    /*
     * Файла нет.
     *
     * Это НЕ ошибка.
     *
     * Просто custom spawn отсутствует,
     * поэтому будет использоваться
     * стандартный spawn.
     */
    if (!file_exists(m_szSpawnFile))
    {
        server_print(
            "[Biohazard] Spawn file '%s' not found. Using default spawns.",
            m_szSpawnFile
        );

        return true;
    }

    new JSON:json =
        json_parse(
            m_szSpawnFile,
            true
        );

    if (json == Invalid_JSON)
    {
        server_print(
            "[Biohazard] Failed to parse '%s'. Using default spawns.",
            m_szSpawnFile
        );

        return false;
    }

    new JSON:maps =
        json_object_get_value(
            json,
            "maps"
        );

    if (!json_is_object(maps))
    {
        if (maps != Invalid_JSON)
            json_free(maps);

        json_free(json);

        server_print(
            "[Biohazard] 'maps' object not found. Using default spawns."
        );

        return true;
    }

    /*
     * Получаем:
     *
     * maps[current_map]
     */
    new JSON:map =
        json_object_get_value(
            maps,
            m_szMapName
        );

    if (!json_is_object(map))
    {
        if (map != Invalid_JSON)
            json_free(map);

        json_free(maps);
        json_free(json);

        server_print(
            "[Biohazard] No custom spawns for map '%s'. Using default spawns.",
            m_szMapName
        );

        return true;
    }

    /*
     * Получаем:
     *
     * maps[current_map].spawns
     */
    new JSON:spawns =
        json_object_get_value(
            map,
            "spawns"
        );

    if (!json_is_array(spawns))
    {
        if (spawns != Invalid_JSON)
            json_free(spawns);

        json_free(map);
        json_free(maps);
        json_free(json);

        server_print(
            "[Biohazard] No spawn array for map '%s'. Using default spawns.",
            m_szMapName
        );

        return true;
    }

    new data[SpawnData];

    for (
        new i = 0;
        i < json_array_get_count(spawns);
        i++
    )
    {
        new JSON:item =
            json_array_get_value(
                spawns,
                i
            );

        if (!json_is_object(item))
        {
            if (item != Invalid_JSON)
                json_free(item);

            continue;
        }

        /*
         * Обязательно обнуляем структуру.
         */
        data[SpawnOrigin][0] = 0.0;
        data[SpawnOrigin][1] = 0.0;
        data[SpawnOrigin][2] = 0.0;

        data[SpawnAngles][0] = 0.0;
        data[SpawnAngles][1] = 0.0;
        data[SpawnAngles][2] = 0.0;

        /*
         * origin
         */
        new JSON:origin =
            json_object_get_value(
                item,
                "origin"
            );

        if (
            json_is_array(origin) &&
            json_array_get_count(origin) >= 3
        )
        {
            for (new j = 0; j < 3; j++)
            {
                data[SpawnOrigin][j] =
                    json_array_get_real(
                        origin,
                        j
                    );
            }
        }

        /*
         * angles
         */
        new JSON:angles =
            json_object_get_value(
                item,
                "angles"
            );

        if (
            json_is_array(angles) &&
            json_array_get_count(angles) >= 3
        )
        {
            for (new j = 0; j < 3; j++)
            {
                data[SpawnAngles][j] =
                    json_array_get_real(
                        angles,
                        j
                    );
            }
        }

        /*
         * Если origin отсутствует —
         * такую точку не добавляем.
         */
        if (
            !json_is_array(origin) ||
            json_array_get_count(origin) < 3
        )
        {
            if (origin != Invalid_JSON)
                json_free(origin);

            if (angles != Invalid_JSON)
                json_free(angles);

            json_free(item);

            continue;
        }

        ArrayPushArray(
            m_arrSpawnPointsVec,
            data
        );

        if (origin != Invalid_JSON)
            json_free(origin);

        if (angles != Invalid_JSON)
            json_free(angles);

        json_free(item);
    }

    json_free(spawns);
    json_free(map);
    json_free(maps);
    json_free(json);

    m_iNumSpawnPoints =
        ArraySize(
            m_arrSpawnPointsVec
        );

    if (m_iNumSpawnPoints)
    {
        server_print(
            "[Biohazard] Loaded %d custom spawn points for '%s'.",
            m_iNumSpawnPoints,
            m_szMapName
        );
    }
    else
    {
        server_print(
            "[Biohazard] Custom spawn points for '%s' not found. Using default spawns.",
            m_szMapName
        );
    }

    return true;
}


/*
 * ============================================================
 * ZOMBIE RESPAWN
 * ============================================================
 */

static stock bool:can_zombie_respawn(id)
{
    new alive =
        get_alive_players() + 1;

    new zombies =
        get_alive_zombies();

    new humans =
        get_alive_humans();

    if (
        alive <= 1 ||
        humans <= 1 ||
        zombies <= 0
    )
    {
        return false;
    }

    if (zombies >= humans)
        return false;

    if (
        zombies + 1 >
        floatround(
            float(alive) *
            get_max_zombie_count(),
            floatround_ceil
        )
    )
    {
        return false;
    }

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

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

    return
        chance > 0.0 &&
        random_float(
            0.0,
            100.0
        ) <= chance;
}


static stock stop_zombie_respawn()
{
    m_bZombieRespawnAllowed = false;

    for (
        new i = 1;
        i <= MaxClients;
        i++
    )
    {
        if (
            is_user_connected(i) &&
            BIT_VALID(
                m_bitZombieRespawn,
                i
            )
        )
        {
            BIT_SUB(
                m_bitZombieRespawn,
                i
            );

            set_member(
                i,
                m_flRespawnPending,
                0.0
            );
        }
    }
}


static stock get_alive_players()
{
    new count;

    for (
        new i = 1;
        i <= MaxClients;
        i++
    )
    {
        if (is_user_alive(i))
            count++;
    }

    return count;
}


static stock get_alive_zombies()
{
    new count;

    for (
        new i = 1;
        i <= MaxClients;
        i++
    )
    {
        if (
            is_user_alive(i) &&
            BIT_VALID(
                m_bitZombie,
                i
            )
        )
        {
            count++;
        }
    }

    return count;
}


static stock get_alive_humans()
{
    new count;

    for (
        new i = 1;
        i <= MaxClients;
        i++
    )
    {
        if (
            is_user_alive(i) &&
            !BIT_VALID(
                m_bitZombie,
                i
            )
        )
        {
            count++;
        }
    }

    return count;
}


static stock get_zombie_player_ratio()
{
    new players =
        get_alive_players();

    if (players <= 4)
        return 33;

    if (players <= 9)
        return 40;

    if (players <= 19)
        return 45;

    return 50;
}


static stock get_zombie_time_ratio()
{
    new Float:start =
        get_member_game(
            m_fRoundStartTime
        );

    if (start <= 0.0)
        return 50;

    new Float:time =
        get_gametime() - start;

    if (time < 30.0)
        return 50;

    if (time < 60.0)
        return 45;

    if (time < 120.0)
        return 40;

    return 35;
}


static stock get_max_zombie_count()
{
    new players =
        get_alive_players();

    if (!players)
        return 0;

    new playerRatio =
        get_zombie_player_ratio();

    new timeRatio =
        get_zombie_time_ratio();

    new ratio =
        playerRatio < timeRatio
            ? playerRatio
            : timeRatio;

    new count =
        floatround(
            float(players) *
            float(ratio) /
            100.0,
            floatround_ceil
        );

    return count < 1
        ? 1
        : count;
}


static stock bool:IsPlaying(id)
{
    new TeamName:team =
        get_member(
            id,
            m_iTeam
        );

    return
        team == TEAM_CT ||
        team == TEAM_TERRORIST;
}


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

    return
        !get_tr2(
            0,
            TR_StartSolid
        ) &&
        !get_tr2(
            0,
            TR_AllSolid
        ) &&
        get_tr2(
            0,
            TR_InOpen
        );
}


static stock give_human_equipment(id)
{
    new const primary_entities[][] =
    {
        "weapon_m3",
        "weapon_xm1014",

        "weapon_mac10",
        "weapon_tmp",
        "weapon_mp5navy",
        "weapon_ump45",
        "weapon_p90",

        "weapon_galil",
        "weapon_famas",
        "weapon_ak47",
        "weapon_m4a1",
        "weapon_sg552",
        "weapon_aug",
        "weapon_scout",
        "weapon_awp",
        "weapon_g3sg1",
        "weapon_sg550",

        "weapon_m249"
    };

    new const secondary_entities[][] =
    {
        "weapon_p228",
        "weapon_usp",
        "weapon_glock18",
        "weapon_deagle",
        "weapon_fiveseven",
        "weapon_elite"
    };

    new const equipments[][] =
    {
        "weapon_hegrenade",
        "weapon_flashbang"
    };

    rg_give_item(
        id,
        "weapon_knife"
    );

    new primary =
        random(
            sizeof primary_entities
        );

    new primary_id =
        get_weaponid(
            primary_entities[primary]
        );

    rg_give_item(
        id,
        primary_entities[primary],
        GT_REPLACE
    );

    rg_set_user_bpammo(
        id,
        any:primary_id,
        999
    );

    new secondary =
        random(
            sizeof secondary_entities
        );

    new secondary_id =
        get_weaponid(
            secondary_entities[secondary]
        );

    rg_give_item(
        id,
        secondary_entities[secondary],
        GT_REPLACE
    );

    rg_set_user_bpammo(
        id,
        any:secondary_id,
        999
    );

    new grenade =
        random(
            sizeof equipments
        );

    rg_give_item(
        id,
        equipments[grenade]
    );

    if (random_num(0, 1))
    {
        new secondGrenade;

        do
        {
            secondGrenade =
                random(
                    sizeof equipments
                );
        }
        while (
            secondGrenade == grenade
        );

        rg_give_item(
            id,
            equipments[secondGrenade]
        );
    }
}


/*
 * ============================================================
 * MONEY NATIVES
 * ============================================================
 */

@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();
}


@native_get_user_money(amxx, argc)
{
    new id =
        get_param(1);

    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)
{
    new id =
        get_param(1);

    if (!is_user_connected(id))
    {
        log_error(
            AMX_ERR_NATIVE,
            "Invalid index '%d'.",
            id
        );

        return 0;
    }

    new amount =
        get_param(2);

    if (amount < 0)
        amount = 0;
    else if (amount > 12000)
        amount = 12000;

    /*
     * m_iMoney — источник истины.
     */
    m_iMoney[id] =
        amount;

    /*
     * Синхронизируем CS account.
     */
    set_member(
        id,
        m_iAccount,
        amount
    );

    /*
     * Отправляем отображение клиенту.
     */
    message_begin(
        MSG_ONE,
        get_user_msgid("Money"),
        .player = id
    );

    write_long(amount);

    write_byte(
        get_param(3)
    );

    message_end();

    return 1;
}