diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7dadc21 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,14 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +custom: [ 'https://www.paypal.me/wopox1337', 'https://yoomoney.ru/to/410011388660403'] + diff --git a/LICENSE b/.github/LICENSE similarity index 100% rename from LICENSE rename to .github/LICENSE diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..9889ade --- /dev/null +++ b/.github/README.md @@ -0,0 +1,66 @@ +

+ + Gun logo + +

+ +

+ AMXModX plugins to provide Deathmatch gameplay in Counter-Strike 1.6 optimized to work with ReGameDLL_CS. +

+ +

+ + Build status + + + Build status + + + Release + + + AMXModX dependency + +

+ +## About +- TODO + +## Features +- TODO + +## Requirements +- HLDS installed; +- [ReGameDLL](https://github.com/s1lentq/ReGameDLL_CS) installed; +- Installed AMXModX ([`v1.9`](https://www.amxmodx.org/downloads-new.php) or [`v1.10`](https://www.amxmodx.org/downloads-new.php?branch=master)); +- Installed [ReAPI](https://github.com/s1lentq/reapi) module; + +## Installation +- [Download the latest](https://github.com/wopox1337/ReDeathmatch/releases/latest) stable version from the release section. +- Extract the `cstrike` folder to the root folder of the HLDS server; +- Make sure that all plugins are running and in the correct order, using the `amxx list` command. + +## Updating +- Put new plugins and lang-files (`plugins/*.amxx` & `data/lang/*.txt`) into `amxmodx/` folder on the HLDS server; +- Restart the server (command `restart` or change the map); +- Make sure that the versions of the plugins are up to date with the command `amxx list`. + +## Downloads +- [Release builds](https://github.com/wopox1337/ReDeathmatch/releases) +- [Dev builds](https://github.com/wopox1337/ReDeathmatch/actions/workflows/build.yml) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..b8b6dbc --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,145 @@ +name: CI + +on: + push: + branches: [master] + paths-ignore: + - "**.md" + + pull_request: + types: [opened, reopened, synchronize] + release: + types: [published] + +jobs: + build: + name: "Build" + runs-on: ubuntu-20.04 + outputs: + COMMIT_SHA: ${{ steps.declare_sha.outputs.COMMIT_SHA }} + SEMVER: ${{ steps.declare_sha.outputs.SEMVER }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Parse SemVer string (release) + id: semver_parser + if: | + github.event_name == 'release' && + github.event.action == 'published' && + startsWith(github.ref, 'refs/tags/') + uses: booxmedialtd/ws-action-parse-semver@v1.4.7 + with: + input_string: ${{ github.ref }} + version_extractor_regex: 'refs\/tags\/(.*)$' + + - name: Declare SHA & package name + id: declare_sha + shell: bash + run: | + SHA=$(git rev-parse --short HEAD) + echo "COMMIT_SHA=$SHA" >> $GITHUB_OUTPUT + echo "SEMVER=${{ steps.semver_parser.outputs.fullversion }}" >> $GITHUB_OUTPUT + + - name: Setup latest ReAPI includes + env: + REPO: "s1lentq/reapi" + run: | + mkdir -p dep/reapi + cd dep/reapi + + curl \ + --silent \ + https://api.github.com/repos/$REPO/releases/latest | \ + grep "browser_download_url" | \ + grep -Eo 'https://[^\"]*' | \ + xargs wget + + 7z x *.zip + + echo "REAPI_INCLUDE_PATH=$(pwd)/addons/amxmodx/scripting/include" >> $GITHUB_ENV + + - name: Update versions for plugins (release) + working-directory: cstrike/addons/amxmodx/scripting/include/ + if: | + github.event_name == 'release' && + github.event.action == 'published' && + startsWith(github.ref, 'refs/tags/') + env: + PLUGIN_VERSION: "${{ steps.declare_sha.outputs.SEMVER }}" + run: sed -i "s|%VERSION%|$PLUGIN_VERSION|g" redm.inc + + - name: Update versions for plugins (only for artifacts builds) + working-directory: cstrike/addons/amxmodx/scripting/include/ + env: + PLUGIN_VERSION: "${{ steps.declare_sha.outputs.COMMIT_SHA }}" + run: sed -i "s|%VERSION%|$PLUGIN_VERSION|g" redm.inc + + - name: Setup AMXXPawn Compiler + uses: wopox1337/setup-amxxpawn@v1 + with: + version: "1.10.5428" + + - name: Compile plugins + working-directory: cstrike/addons/amxmodx/scripting/ + env: + REAPI_INCLUDE: ${{ env.REAPI_INCLUDE_PATH }} + run: | + compile() { + sourcefile=$1 + amxxfile="$(echo $sourcefile | sed -e 's/\.sma$/.amxx/')" + output_path="../plugins/$amxxfile" + + mkdir -p $(dirname $output_path) + + echo -n "Compiling $sourcefile ... " + amxxpc $sourcefile -o"$output_path" \ + -i"include" \ + -i"$REAPI_INCLUDE" + } + export -f compile + + find * -type f -name "*.sma" -exec bash -c 'compile "$0"' {} \; + + - name: Move files + run: | + mkdir publish + mv cstrike/ publish/ + + - name: Deploy artifact + uses: actions/upload-artifact@v3.1.2 + with: + name: ReDeathatch-${{ steps.declare_sha.outputs.COMMIT_SHA }}-dev + path: publish/* + + publish: + name: "Publish release" + runs-on: ubuntu-20.04 + needs: [build] + if: | + github.event_name == 'release' && + github.event.action == 'published' && + startsWith(github.ref, 'refs/tags/') + steps: + - name: Download artifact + uses: actions/download-artifact@v3.0.2 + with: + name: ReDeathatch-${{ needs.build.outputs.COMMIT_SHA }}-dev + + - name: Packaging binaries + id: packaging + run: 7z a -mm=Deflate -mfb=258 -mpass=15 -r ReDeathatch-${{ needs.build.outputs.SEMVER }}.zip cstrike/ + + - name: Publish artifacts + uses: softprops/action-gh-release@v1 + id: publish-job + if: | + startsWith(github.ref, 'refs/tags/') && + steps.packaging.outcome == 'success' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + files: | + *.zip diff --git a/cstrike/addons/amxmodx/configs/plugins-redm.ini b/cstrike/addons/amxmodx/configs/plugins-redm.ini new file mode 100644 index 0000000..3bc1f1c --- /dev/null +++ b/cstrike/addons/amxmodx/configs/plugins-redm.ini @@ -0,0 +1,5 @@ +; Main plugin +ReDeathmatch.amxx debug + +; Addons +redm_spawns.amxx debug diff --git a/cstrike/addons/amxmodx/configs/redm/gamemode_deathmatch.json b/cstrike/addons/amxmodx/configs/redm/gamemode_deathmatch.json new file mode 100644 index 0000000..7cf6dd8 --- /dev/null +++ b/cstrike/addons/amxmodx/configs/redm/gamemode_deathmatch.json @@ -0,0 +1,137 @@ +{ + "equip": { + "primary": [ + "weapon_m4a1", + "weapon_ak47", + "weapon_famas", + "weapon_galil", + "weapon_awp", + "weapon_mp5navy", + "weapon_p90", + "weapon_aug", + "weapon_sg552", + "weapon_scout", + "weapon_ump45", + "weapon_sg550", + "weapon_m249", + "weapon_g3sg1", + "weapon_m3", + "weapon_xm1014", + "weapon_tmp", + "weapon_mac10" + ], + "secondary": [ + "weapon_usp", + "weapon_glock18", + "weapon_deagle", + "weapon_p228", + "weapon_elite", + "weapon_fiveseven" + ] + }, + "cvars": { + "developer": "0", + "mp_round_restart_delay": "7.0", + "bot_stop": "0", + "bot_quota": "18", + "mp_roundtime": "2.25", + "mp_freeforall": "1", + "bot_chatter": "off", + "bot_deathmatch": "1", + "bot_defer_to_human": "0", + "bot_difficulty": "3", + "bot_join_after_player": "0", + "bot_quota_mode": "fill", + "motdfile": "", + "mp_auto_join_team": "1", + "mp_autokick": "0", + "mp_autokick_timeout": "0", + "mp_autoteambalance": "0", + "mp_buytime": "0", + "mp_buy_anywhere": "1", + "mp_ct_default_grenades": "", + "mp_t_default_weapons_primary": "ak47", + "mp_ct_default_weapons_primary": "m4a1", + "mp_t_default_weapons_secondary": "glock18", + "mp_ct_default_weapons_secondary": "usp", + "mp_forcerespawn": "0.5", + "mp_free_armor": "2", + "mp_freezetime": "0", + "mp_friendlyfire": "0", + "mp_give_player_c4": "0", + "mp_infinite_ammo": "2", + "mp_item_staytime": "0", + "mp_limitteams": "0", + "mp_startmoney": "0", + "mp_playerid": "0", + "mp_radio_maxinround": "0", + "mp_refill_bpammo_weapons": "2", + "mp_round_infinite": "bcdefg", + "mp_scoreboard_showhealth": "0", + "mp_scoreboard_showmoney": "0", + "mp_show_radioicon": "0", + "mp_t_default_grenades": "", + "mp_timelimit": "0", + "mp_weapons_allow_map_placed": "0", + "mp_damage_headshot_only": "0" + }, + "modes": { + "Pistols - HS ONLY": { + "equip": { + "secondary": [ + "weapon_usp", + "weapon_glock18", + "weapon_deagle", + "weapon_p228", + "weapon_elite", + "weapon_fiveseven" + ] + }, + "cvars": { + "mp_damage_headshot_only": "1", + "mp_t_default_weapons_primary": "", + "mp_ct_default_weapons_primary": "", + "mp_free_armor": "0" + } + }, + "Second round weapons": { + "equip": { + "primary": [ + "weapon_scout", + "weapon_famas", + "weapon_galil", + "weapon_mp5navy", + "weapon_p90", + "weapon_ump45", + "weapon_m3", + "weapon_xm1014", + "weapon_tmp", + "weapon_mac10" + ], + "secondary": [ + "weapon_usp", + "weapon_glock18", + "weapon_deagle", + "weapon_p228", + "weapon_elite", + "weapon_fiveseven" + ] + }, + "cvars": { + "mp_t_default_weapons_primary": "scout", + "mp_ct_default_weapons_primary": "scout", + "mp_t_default_weapons_secondary": "deagle", + "mp_ct_default_weapons_secondary": "deagle", + "mp_free_armor": "1" + } + }, + "All weapons - HS ONLY": { + "cvars": { + "mp_damage_headshot_only": "1" + } + }, + "All weapons": { + + } + } +} diff --git a/cstrike/addons/amxmodx/data/lang/redm.txt b/cstrike/addons/amxmodx/data/lang/redm.txt new file mode 100644 index 0000000..5f06a1f --- /dev/null +++ b/cstrike/addons/amxmodx/data/lang/redm.txt @@ -0,0 +1,25 @@ +[en] +Currently = currently +Already = already + +RandomWeapons = Random weapon +Enabled = enabled +Disabled = disabled +PrimaryEquip = Primary equip +SecondaryEquip = Secondary equip +ToClosePressG = \wTo close the menu - press `\yG\w` +CurrentMode = Current mode +GunsHelp = Say ^3guns ^1to open equip menu. + +[ru] +Currently = сейчас +Already = уже + +RandomWeapons = Случайные оружия +Enabled = включено +Disabled = отключено +PrimaryEquip = Основное оружие +SecondaryEquip = Дополнительное оружие +ToClosePressG = \wЧтобы закрыть меню - нажмите `\yG\w` +CurrentMode = Текущий режим +GunsHelp = Напишите ^3guns ^1чтобы открыть меню экипировки. diff --git a/cstrike/addons/amxmodx/scripting/ReDeathmatch.sma b/cstrike/addons/amxmodx/scripting/ReDeathmatch.sma new file mode 100644 index 0000000..e71f3c9 --- /dev/null +++ b/cstrike/addons/amxmodx/scripting/ReDeathmatch.sma @@ -0,0 +1,182 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "ReDeathmatch/ReDM_config.inc" +#include "ReDeathmatch/ReDM_cvars_handler.inc" +#include "ReDeathmatch/ReDM_spawn_manager.inc" +#include "ReDeathmatch/ReDM_equip_manager.inc" +#include "ReDeathmatch/ReDM_features.inc" +#include "ReDeathmatch/ReDM_round_modes.inc" +#include "ReDeathmatch/ReDM_api.inc" + +static g_pcvar_redm_active + +static bool: g_prevState + +static const g_soundEffects[][] = { + "fvox/blip.wav", + "buttons/button9.wav", + "buttons/bell1.wav", +} + +public plugin_init() { + register_plugin("ReDeathmatch", VERSION, "Sergey Shorokhov") + register_dictionary("redm.txt") + + create_cvar("redm_version", VERSION, (FCVAR_SERVER|FCVAR_SPONLY)) + + ApiInit_Forwards() + + Config_Init() +} + +public plugin_precache() { + Features_Precache() + + for (new i; i < sizeof(g_soundEffects); i++) + precache_sound(g_soundEffects[i]) +} + +public plugin_cfg() { + CvarsHandler_Init() + CallApi_InitStart() + + Features_Init() + SpawnManager_Init() + RoundModes_Init() + EquipManager_Init() + + RegisterHookChain(RG_CSGameRules_PlayerKilled, "CSGameRules_PlayerKilled_Post", .post = true) + + g_pcvar_redm_active = create_cvar("redm_active", "0", (FCVAR_SERVER|FCVAR_SPONLY)) + + register_concmd("redm_enable", "ConCmd_redm_enable", ADMIN_MAP, "Enables Re:DM.") + register_concmd("redm_disable", "ConCmd_redm_disable", ADMIN_MAP, "Disables Re:DM.") + register_concmd("redm_status", "ConCmd_redm_status", ADMIN_MAP, "Get Re:DM status.") + register_concmd("redm", "ConCmd_redm", ADMIN_ALL, "Get info.") + + CallApi_Initialized() + + if (Config_GetCurrent() != Invalid_JSON) + SetActive(true) +} + +public plugin_end() { + RestoreAllCvars() +} + +public plugin_pause() { + if (!IsActive()) + return + + g_prevState = true + SetActive(false) +} + +public plugin_unpause() { + if (!g_prevState) + return + + SetActive(true) +} + +public client_putinserver(player) { + EquipManager_PutInServer(player) +} + +public CSGameRules_PlayerKilled_Post(const victim, const killer, const inflictor) { + if (!IsActive()) + return + + if (!killer || killer == victim) + return + + Features_PlayerKilled(victim, killer) + EquipManager_PlayerKilled(victim, killer) +} + +public ConCmd_redm_enable(const player, const level, const cid) { + SetGlobalTransTarget(player) + + if (!cmd_access(player, level, cid, 1)) + return PLUGIN_HANDLED + + if (IsActive()) { + console_print(player, " * ReDeathmatch %l - `%l`!", + "Already", "Enabled" + ) + + return PLUGIN_HANDLED + } + + SetActive(true) + return PLUGIN_HANDLED +} + +public ConCmd_redm_disable(const player, level, cid) { + SetGlobalTransTarget(player) + + if (!cmd_access(player, level, cid, 1)) + return PLUGIN_HANDLED + + if (!IsActive()) { + console_print(player, " * ReDeathmatch %l - `%l`!", + "Already", "Disabled" + ) + + return PLUGIN_HANDLED + } + + SetActive(false) + return PLUGIN_HANDLED +} + +public ConCmd_redm_status(const player, level, cid) { + SetGlobalTransTarget(player) + + console_print(player, "* ReDeathmatch %l - `%l`.", + "Currently", + IsActive() ? "Enabled" : "Disabled" + ) + + return PLUGIN_HANDLED +} + +public ConCmd_redm(const player, level, cid) { + SetGlobalTransTarget(player) + + console_print(player, "[Re:DM] Version `%s`", VERSION) + console_print(player, "[Re:DM] https://github.com/wopox1337/ReDeathmatch") + console_print(player, "[Re:DM] Copyright (c) 2023 Sergey Shorokhov", VERSION) + + return PLUGIN_HANDLED +} + +bool: IsActive() { + return get_pcvar_num(g_pcvar_redm_active) != 0 +} + +SetActive(const bool: active) { + CallApi_ChangeState(active) + + ApplyState(active) + + set_pcvar_num(g_pcvar_redm_active, active ? 1 : 0) +} + +static ApplyState(const bool: active) { + if (active) { + ReloadConfig() + } else { + RestoreAllCvars() + } + + set_cvar_num("sv_restart", 1) +} diff --git a/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_api.inc b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_api.inc new file mode 100644 index 0000000..f78ba1c --- /dev/null +++ b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_api.inc @@ -0,0 +1,161 @@ +static g_fwdChangeState = -1 +static g_fwdInitStart = -1 +static g_fwdInitialized = -1 +static g_fwdConfigLoaded = -1 +static g_fwdGetConfigName = -1 +static g_fwdGetConfigPrefixName = -1 + + +public plugin_natives() { + register_native("redm_active", "native_redm_active") + register_native("redm_set_active", "native_redm_set_active") + register_native("redm_get_config", "native_redm_get_config") + register_native("redm_get_equip_config", "native_redm_get_equip_config") + register_native("redm_get_round_mode_id", "native_redm_get_round_mode_id") + + register_native("redm_spawnstyles", "native_redm_spawnstyles") + register_native("redm_styleinfo", "native_redm_styleinfo") + register_native("redm_addstyle", "native_redm_addstyle") + register_native("redm_setstyle", "native_redm_setstyle") + register_native("redm_curstyle", "native_redm_curstyle") +} + +ApiInit_Forwards() { + g_fwdChangeState = CreateMultiForward("ReDM_ChangeState", ET_IGNORE, FP_CELL) + g_fwdInitStart = CreateMultiForward("ReDM_InitStart", ET_IGNORE) + g_fwdInitialized = CreateMultiForward("ReDM_Initialized", ET_IGNORE) + g_fwdConfigLoaded = CreateMultiForward("redm_config_loaded", ET_IGNORE) + g_fwdGetConfigName = CreateMultiForward("ReDM_GetConfigName", ET_IGNORE, FP_ARRAY, FP_CELL) + g_fwdGetConfigPrefixName = CreateMultiForward("ReDM_GetConfigPrefixName", ET_IGNORE, FP_ARRAY, FP_CELL, FP_STRING) +} + +public bool: native_redm_active(const plugin_id, const argc) { + return IsActive() +} + +public bool: native_redm_set_active(const plugin_id, const argc) { + new bool: active = bool: get_param(1) + + if (IsActive() && active) + return false + + if (!IsActive() && !active) + return false + + SetActive(active) + + return true +} + +public JSON: native_redm_get_config(const plugin_id, const argc) { + return Config_GetCurrent() +} + +public JSON: native_redm_get_equip_config(const plugin_id, const argc) { + return Invalid_JSON // TODO: +} + +public native_redm_get_round_mode_id(const plugin_id, const argc) { + return RoundModes_GetCurrentMode() +} + +public native_redm_spawnstyles(const plugin_id, const argc) { + return ArraySize(SpawnManager_Get()) +} + +public native_redm_styleinfo(const plugin_id, const argc) { + new idx = get_param(1) + if (idx < 0 || idx >= ArraySize(SpawnManager_Get())) { + log_error(AMX_ERR_BOUNDS, "Invalid style index provided (%i)", idx) + } + + new spawnStyle[SpawnStyle_s] + ArrayGetArray(SpawnManager_Get(), idx, spawnStyle) + + return set_string(2, spawnStyle[ss_Name], get_param(3)) +} + +public native_redm_addstyle(const plugin_id, const argc) { + new name[32] + get_string(1, name, charsmax(name)) + + new functionName[32] + get_string(2, functionName, charsmax(functionName)) + + new handle = CreateOneForward(plugin_id, functionName, FP_CELL) + if (handle < 0) { + log_error(AMX_ERR_NOTFOUND, "Callback function not found (`%s`)", functionName) + } + + SpawnManager_AddMethod(name, handle) + return handle +} + +public bool: native_redm_setstyle(const plugin_id, const argc) { + new name[32] + get_string(1, name, charsmax(name)) + + if (strcmp(name, "none") == 0) { + SpawnManager_SetCurrentMethodIdx(-1) + + return true + } + + for (new i, size = ArraySize(SpawnManager_Get()); i < size; i++) { + new spawnStyle[SpawnStyle_s] + ArrayGetArray(SpawnManager_Get(), i, spawnStyle) + + if (strcmp(spawnStyle[ss_Name], name) == 0) { + SpawnManager_SetCurrentMethodIdx(i) + return true + } + } + + return false +} + +public native_redm_curstyle(const plugin_id, const argc) { + return SpawnManager_GetCurrentMethodIdx() +} + +CallApi_InitStart() { + ExecuteForward(g_fwdInitStart) +} + +CallApi_Initialized() { + ExecuteForward(g_fwdInitialized) +} + +// TODO +stock CallApi_ConfigLoaded() { + ExecuteForward(g_fwdConfigLoaded) +} + +CallApi_GetConfigName(fileName[], len) { + ExecuteForward( + g_fwdGetConfigName, _, + PrepareArray( + fileName, + len, + .copyback = true + ), + len + ) +} + +CallApi_GetConfigPrefixName(fileName[], len, const mapPrefix[]) { + ExecuteForward( + g_fwdGetConfigPrefixName, _, + PrepareArray( + fileName, + len, + .copyback = true + ), + len, + mapPrefix + ) +} + +CallApi_ChangeState(const bool: active) { + ExecuteForward(g_fwdChangeState, _, active) +} diff --git a/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_config.inc b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_config.inc new file mode 100644 index 0000000..e88a10a --- /dev/null +++ b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_config.inc @@ -0,0 +1,222 @@ +new const g_mainDir[] = "redm" +static const g_extraConfigsDir[] = "extraconfigs" +static const g_mainConfigFile[] = "gamemode_deathmatch" + +new JSON: g_configHandle = Invalid_JSON + +Config_Init() { + ParseConfig() + + register_concmd("redm_reload", "ConCmd_redm_reload", ADMIN_MAP, " - reloads Re:DM config") +} + +static FindConfigFile(filePath[], len = PLATFORM_MAX_PATH) { + // TODO: refactor it by a cycle + + new configsDir[PLATFORM_MAX_PATH] + get_configsdir(configsDir, charsmax(configsDir)) + + new configFile[PLATFORM_MAX_PATH] + formatex(configFile, charsmax(configFile), + "%s/%s/%s", + configsDir, g_mainDir, g_extraConfigsDir + ) + + if (!dir_exists(configFile) && mkdir(configFile) == -1) + LogMessageEx(Warning, "FindConfigFile: Can't create directory `%s`.", configFile) + + // Check configs/redm/extraconfigs/.json file + new mapName[MAX_MAPNAME_LENGTH] + rh_get_mapname(mapName, charsmax(mapName)) + + formatex(configFile, charsmax(configFile), "%s/%s/%s/%s.json", + configsDir, g_mainDir, g_extraConfigsDir, mapName + ) + + new fileName[PLATFORM_MAX_PATH] + + if (file_exists(configFile)) { + remove_filepath(configFile, fileName, charsmax(fileName)) + LogMessageEx(Info, "FindConfigFile: Extra map config loaded for map `%s`.", fileName) + copy(filePath, len, configFile) + + return true + } + + // Check configs/redm/extraconfigs/prefix_.json file + new mapPrefix[32] + copyc(mapPrefix, charsmax(mapPrefix), mapName, '_') + formatex(configFile, charsmax(configFile), "%s/%s/%s/%s.json", + configsDir, g_mainDir, g_extraConfigsDir, GetMainConfigPrefixName(mapPrefix) + ) + + if (file_exists(configFile)) { + remove_filepath(configFile, fileName, charsmax(fileName)) + LogMessageEx(Info, "FindConfigFile: Prefix map config `%s` loaded.", fileName) + copy(filePath, len, configFile) + + return true + } + + // Check configs/redm/gamemode_deathmatch.json file + formatex(configFile, charsmax(configFile), "%s/%s/%s.json", + configsDir, g_mainDir, GetMainConfigName() + ) + + if (file_exists(configFile)) { + remove_filepath(configFile, fileName, charsmax(fileName)) + LogMessageEx(Info, "FindConfigFile: Config `%s` loaded.", fileName) + copy(filePath, len, configFile) + + return true + } + + LogMessageEx(Warning, "FindConfigFile: Can't find any config file!") + + return false +} + +static GetMainConfigName() { + new file[PLATFORM_MAX_PATH] + copy(file, charsmax(file), g_mainConfigFile) + + CallApi_GetConfigName(file, charsmax(file)) + + return file +} + +static GetMainConfigPrefixName(const mapPrefix[]) { + new file[PLATFORM_MAX_PATH] + format(file, charsmax(file), "prefix_%s", mapPrefix) + + CallApi_GetConfigPrefixName(file, charsmax(file), mapPrefix) + + return file +} + +static bool: ParseConfig(const file[] = "") { + new configFile[PLATFORM_MAX_PATH] + + if (strlen(file) != 0) { + copy(configFile, charsmax(configFile), file) + } else if (!FindConfigFile(configFile, charsmax(configFile))) { + return false + } + + new JSON: config = json_parse(configFile, .is_file = true, .with_comments = true) + if (config == Invalid_JSON) { + LogMessageEx(Warning, "ParseConfig: Can't parse JSON from `%s` file!", configFile) + + return false + } + + g_configHandle = config + return true +} + +ReloadConfig() { + CvarsHandler_LoadConfig(Config_GetCurrent()) + + new JSON: equipConfig = Config_GetModeEquip() + if (equipConfig == Invalid_JSON) + equipConfig = Config_GetMainEquip() + + EquipManager_LoadConfig(equipConfig) + + json_free(equipConfig) +} + + +public ConCmd_redm_reload(const player, level, cid) { + SetGlobalTransTarget(player) + + if (!cmd_access(player, level, cid, 2)) + return PLUGIN_HANDLED + + new filePath[PLATFORM_MAX_PATH] + if (read_argc() == 2) { + read_argv(1, filePath, charsmax(filePath)) + } + + if (!Config_ReloadCfg(filePath)) + return PLUGIN_HANDLED + + RoundModes_ResetCurrentMode() + console_print(player, "[Re:DM] Config file reloaded.") + return PLUGIN_HANDLED +} + +bool: Config_ReloadCfg(const file[] = "") { + json_free(g_configHandle) + + if (strlen(file) == 0) + return ParseConfig() + + new configsDir[PLATFORM_MAX_PATH] + get_configsdir(configsDir, charsmax(configsDir)) + + new configFile[PLATFORM_MAX_PATH] + formatex(configFile, charsmax(configFile), + "%s/%s/%s", + configsDir, g_mainDir, file + ) + + if (!file_exists(configFile)) { + LogMessageEx(Warning, "Config_ReloadCfg: Config file `%s` not exists!", configFile) + + return false + } + + return ParseConfig(configFile) +} + +JSON: Config_GetCurrent() { + return g_configHandle +} + +JSON: Config_GetModeEquip() { + if (!json_object_has_value(Config_GetCurrent(), "modes")) { + return Invalid_JSON + } + + new JSON: objModes = json_object_get_value(Config_GetCurrent(), "modes") + new count = json_object_get_count(objModes) + if (!count) { + json_free(objModes) + + return Invalid_JSON + } + + new currentModeIdx = RoundModes_GetCurrentMode() + if (currentModeIdx < 0) { + json_free(objModes) + + return Invalid_JSON + } + + + new JSON: objMode = json_object_get_value_at(objModes, currentModeIdx) + if (!json_object_has_value(objMode, "equip")) { + json_free(objMode) + json_free(objModes) + + return Invalid_JSON + } + + new JSON: objEquip = json_object_get_value(objMode, "equip") + new JSON: obj = json_deep_copy(objEquip) + + json_free(objEquip) + json_free(objMode) + json_free(objModes) + + return obj +} + +JSON: Config_GetMainEquip() { + if (!json_object_has_value(Config_GetCurrent(), "equip")) + return Invalid_JSON + + new JSON: objEquip = json_object_get_value(Config_GetCurrent(), "equip") + return objEquip +} \ No newline at end of file diff --git a/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_cvars_handler.inc b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_cvars_handler.inc new file mode 100644 index 0000000..bd21376 --- /dev/null +++ b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_cvars_handler.inc @@ -0,0 +1,145 @@ +static Trie: g_cvarValues = Invalid_Trie + + +CvarsHandler_Init() { + register_concmd("redm_dump_cvars", "ConCmd_redm_dump_cvars", ADMIN_MAP, "Dump changed CVar to table.") +} + +public ConCmd_redm_dump_cvars(const player) { + DumpAllSavedCvars(player) + + return PLUGIN_HANDLED +} + +CvarsHandler_LoadConfig(const JSON: config) { + new JSON: cvars = json_object_get_value(config, "cvars") + if (cvars == Invalid_JSON) { + set_fail_state("Can't read `cvars` section from config") + } + + CvarsHandler_LoadCvars(cvars) + + json_free(cvars) +} + +CvarsHandler_LoadCvars(const JSON: objCvars) { + new cvarsCount = json_object_get_count(objCvars) + if (!cvarsCount) { + // set_fail_state("Section `cvars` hasn't settings in config") + return + } + + for (new i; i < cvarsCount; i++) { + new JSON: cvarSetting = json_object_get_value_at(objCvars, i) + + new key[32], value[32] + json_object_get_name(objCvars, i, key, charsmax(key)) + json_get_string(cvarSetting, value, charsmax(value)) + + CvarChangeValue(key, value) + + json_free(cvarSetting) + } +} + +static CvarChangeValue(const cvar[], const value[]) { + if (!IsActive()) { + if (!g_cvarValues) { + g_cvarValues = TrieCreate() + } + + new oldValue[32] + get_cvar_string(cvar, oldValue, charsmax(oldValue)) + + if (strcmp(oldValue, value) == 0) + return + + new storedValue[32] + if (TrieGetString(g_cvarValues, cvar, storedValue, charsmax(storedValue))) { + LogMessageEx(Warning, "CvarChangeValue: WARNING! CVar `%s` already has stored value: `%s` and been replaced by `%s` ", + cvar, storedValue, value + ) + } + + TrieSetString(g_cvarValues, cvar, oldValue) + } + + set_cvar_string(cvar, value) +} + +static stock bool: CvarRestoreValue(const cvar[]) { + new storedValue[32] + if (!TrieGetString(g_cvarValues, cvar, storedValue, charsmax(storedValue))) { + LogMessageEx(Warning, "CvarRestoreValue: WARNING! CVar `%s` hasn't stored value. Can't restore.", + cvar, storedValue, value + ) + + return false + } + + set_cvar_string(cvar, storedValue) + + return true +} + +RestoreAllCvars() { + if (g_cvarValues == Invalid_Trie) { + LogMessageEx(Debug, "RestoreAllCvars(): WARNING! CVars not saved. Can't restore.") + return + } + + new TrieIter: iter = TrieIterCreate(g_cvarValues) + + while(!TrieIterEnded(iter)) { + new key[32] + TrieIterGetKey(iter, key, charsmax(key)) + + new value[32] + TrieIterGetString(iter, value, charsmax(value)) + + set_cvar_string(key, value) + + TrieIterNext(iter) + } + + TrieIterDestroy(iter) + TrieClear(g_cvarValues) +} + +stock DumpAllSavedCvars(const player = 0) { + if (g_cvarValues == Invalid_Trie) { + LogMessageEx(Debug, "DumpAllSavedCvars(): WARNING! CVars not saved. Can't restore.") + return + } + + if (!TrieGetSize(g_cvarValues)) { + LogMessageEx(Debug, "DumpAllSavedCvars(): WARNING! Hasn't saved CVars.") + return + } + + new idx + new TrieIter: iter = TrieIterCreate(g_cvarValues) + console_print(player, "Dump saved CVars:") + + new const template[] = "| %-2i | %-32s | %-8s | %-8s |" + console_print(player, "| %-2s | %-32s | %-8s | %-8s |", + "#", "CVar", "old", "current" + ) + console_print(player, "| -- | -------------------------------- | -------- | -------- |") + while(!TrieIterEnded(iter)) { + new key[32] + TrieIterGetKey(iter, key, charsmax(key)) + + new value[32] + TrieIterGetString(iter, value, charsmax(value)) + + new current[32] + get_cvar_string(key, current, charsmax(current)) + + console_print(player, template, ++idx, key, value, current) + + TrieIterNext(iter) + } + + TrieIterDestroy(iter) +} diff --git a/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_equip_manager.inc b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_equip_manager.inc new file mode 100644 index 0000000..cbbfc71 --- /dev/null +++ b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_equip_manager.inc @@ -0,0 +1,463 @@ +enum EquipType_e { + et_Primary, + et_Secondary, +} +static const g_equipSections[EquipType_e][] = { + "primary", + "secondary" +} + +enum { + EQUIP_NOT_CHOOSEN = -1, +} + +static g_equip[EquipType_e] +static g_playerWeapons[MAX_PLAYERS + 1][EquipType_e] // = []{ EQUIP_NOT_CHOOSEN, ... } + +static InventorySlotType: g_lastWeaponSlot[MAX_PLAYERS + 1] = { PRIMARY_WEAPON_SLOT, ... } +static bool: g_playerRandomWeapons[MAX_PLAYERS + 1] = { false, ... } +static WeaponState: g_weaponState[MAX_PLAYERS + 1] +static bool: g_inEquipMenu[MAX_PLAYERS + 1] + +static redm_keep_weapon_slot = 1 + +EquipManager_Init() { + RegisterHookChain(RG_CBasePlayer_GiveDefaultItems, "CBasePlayer_GiveDefaultItems", .post = false) + // RegisterHookChain(RG_CBasePlayer_GiveNamedItem, "CBasePlayer_GiveNamedItem", .post = false) + + bind_pcvar_num(create_cvar("redm_keep_weapon_slot", "1"), redm_keep_weapon_slot) + + register_concmd("redm_dump_equip", "ConCmd_redm_dump_equip", ADMIN_MAP, "Dump loaded equipset from config to table.") + + register_clcmd("drop", "ClCmd_Drop") + register_clcmd("cl_autobuy", "ClCmd_cl_autobuy") +} + +EquipManager_PutInServer(const player) { + EquipManager_PlayerResetEquip(player) + g_lastWeaponSlot[player] = PRIMARY_WEAPON_SLOT + g_weaponState[player] = (WPNSTATE_USP_SILENCED | WPNSTATE_M4A1_SILENCED) +} + +EquipManager_LoadConfig(const JSON: objEquip) { + for (new EquipType_e: section; section < EquipType_e; section++) { + LoadConfigEquip(objEquip, section) + } +} + +static LoadConfigEquip(const JSON: objEquip, const EquipType_e: section) { + if (g_equip[section] == _: Invalid_Array) { + g_equip[section] = _: ArrayCreate(32) + } + + ArrayClear(Array: g_equip[section]) + + if (!json_object_has_value(objEquip, g_equipSections[section])) + return + + new JSON: equipWeapons = json_object_get_value(objEquip, g_equipSections[section]) + if (equipWeapons == Invalid_JSON) { + set_fail_state("Can't read `%s` section from config", g_equipSections[section]) + } + + new count = json_array_get_count(equipWeapons) + if (!count) { + json_free(equipWeapons) + return + } + + for (new i; i < count; i++) { + new weapon[32] + json_array_get_string(equipWeapons, i, weapon, charsmax(weapon)) + + new weaponId = rg_get_weapon_info(weapon, WI_ID) + if (!weaponId) { + LogMessageEx(Warning, "LoadConfigEquip(): WARNING! Weapon `%s` hasn't ID. Invalid weapon skipped.", + weapon + ) + + continue + } + + if (ArrayFindString(Array: g_equip[section], weapon) != -1) { + LogMessageEx(Warning, "LoadConfigEquip(): WARNING! Weapon `%s` already in equip `%s` list. Skipped.", + weapon, g_equipSections[section] + ) + + continue + } + + ArrayPushString(Array: g_equip[section], weapon) + } + + json_free(equipWeapons) +} + +public ClCmd_cl_autobuy(const player) { + if (!IsActive()) + return PLUGIN_CONTINUE + + Player_SwitchRandomWeapons(player) + + return PLUGIN_HANDLED +} + +public ClCmd_Drop(const player) { + if (!IsActive()) + return PLUGIN_CONTINUE + + Player_CallEquipMenu(player) + + return PLUGIN_HANDLED +} + +static bool: Player_SwitchRandomWeapons(const player) { + if (!IsActive()) + return false + + SetGlobalTransTarget(player) + + g_playerRandomWeapons[player] = !g_playerRandomWeapons[player] + + if (!g_playerRandomWeapons[player]) { + EquipManager_PlayerResetEquip(player) + } + + UTIL_PlaySoundEffect(player, "buttons/button9.wav", .pitch = g_playerRandomWeapons[player] ? PITCH_HIGH : PITCH_LOW) + + client_print_color(player, print_team_red, "[Re:DM] %l - ^3%l^1.", + "RandomWeapons", + g_playerRandomWeapons[player] ? "Enabled" : "Disabled" + ) + + return g_playerRandomWeapons[player] +} + +static bool: Player_CallEquipMenu(const player) { + if (!IsActive()) + return false + + new menuId, newMenuId + player_menu_info(player, menuId, newMenuId) + + if (newMenuId == -1) { + g_inEquipMenu[player] = false + } + + if (g_inEquipMenu[player] || newMenuId != -1) { + UTIL_PlaySoundEffect(player, "fvox/blip.wav", .pitch = 30) + reset_menu(player) + + return false + } + + if (Player_HasChosenEquip(player)) { + EquipManager_PlayerResetEquip(player) + } + + for (new EquipType_e: section; section < EquipType_e; section++) { + if (!HasEquipItems(section)) + continue + + Menu_ChooseEquip(player, section) + + return true + } + + return true +} + +static Menu_ChooseEquip(const player, const EquipType_e: section) { + SetGlobalTransTarget(player) + + new menu = menu_create( + fmt("%l^n%l", + section == et_Primary ? "PrimaryEquip" : "SecondaryEquip", + "ToClosePressG" + ), + "MenuHandler_ChooseEquip" + ) + + static callback + if(!callback) + callback = menu_makecallback("MenuCallback_Primary") + + for (new i, size = ArraySize(Array: g_equip[section]); i < size; i++) { + new equipName[32] + ArrayGetString(Array: g_equip[section], i, equipName, charsmax(equipName)) + + new weaponId = rg_get_weapon_info(equipName, WI_ID) + + new weaponName[32] + rg_get_weapon_info(weaponId, WI_NAME, weaponName, charsmax(weaponName)) + + menu_additem(menu, weaponName, .info = fmt("%i", section), .callback = callback) + } + + menu_setprop(menu, MPROP_BACKNAME, fmt("%l", "BACK")) + menu_setprop(menu, MPROP_NEXTNAME, fmt("%l", "MORE")) + menu_setprop(menu, MPROP_EXITNAME, fmt("%l", "EXIT")) + menu_setprop(menu, MPROP_NUMBER_COLOR, "\y") + + g_inEquipMenu[player] = true + menu_display(player, menu) + + return PLUGIN_HANDLED +} + +public MenuCallback_Primary(const player, const menu, const item) { + new name[32] + menu_item_getinfo( + menu, item, + .name = name, + .namelen = charsmax(name) + ) + + if (strncmp(name, "weapon_", 7) == 0) { + replace(name, charsmax(name), "weapon_", "") + strtoupper(name) + } + + menu_item_setname(menu, item, name) + + return ITEM_IGNORE +} + +public MenuHandler_ChooseEquip(const player, const menu, const item) { + new info[2] + menu_item_getinfo( + menu, item, + .info = info, + .infolen = charsmax(info) + ) + + g_inEquipMenu[player] = false + menu_destroy(menu) + + if(item < 0) { + UTIL_PlaySoundEffect(player, "fvox/blip.wav", .pitch = 30) + + return PLUGIN_HANDLED + } + + new EquipType_e: section = EquipType_e: strtol(info) + g_playerWeapons[player][section] = item + + Player_GiveWeapon(player, section) + + UTIL_PlaySoundEffect(player, "fvox/blip.wav", .pitch = 80) + + new bool: hasNextSection = bool: (++section % EquipType_e) + if (!hasNextSection) { + client_print_color(player, print_team_red, "[Re:DM] %l", + "GunsHelp" + ) + + return PLUGIN_HANDLED + } + + if (!HasEquipItems(section)) + return PLUGIN_HANDLED + + Menu_ChooseEquip(player, section) + + return PLUGIN_HANDLED +} + +public CBasePlayer_GiveDefaultItems(const player) { + if (!IsActive()) + return HC_CONTINUE + + if (is_user_bot(player)) + return HC_CONTINUE + + if (redm_keep_weapon_slot) + RequestFrame("Player_ForceSlotChoose", player) + + // TODO: rework it + if (g_playerRandomWeapons[player]) { + for (new EquipType_e: section; section < EquipType_e; section++) { + if (!HasEquipItems(section)) + continue + + g_playerWeapons[player][section] = random_num(0, ArraySize(Array: g_equip[section]) - 1) + } + } + + for (new EquipType_e: section; section < EquipType_e; section++) { + if (!HasEquipItems(section)) + continue + + if (g_playerRandomWeapons[player]) { + g_playerWeapons[player][section] = random_num(0, ArraySize(Array: g_equip[section]) - 1) + } + + if (g_playerWeapons[player][section] == EQUIP_NOT_CHOOSEN) { + Menu_ChooseEquip(player, section) + + return HC_CONTINUE + } + } + + rg_give_item(player, "weapon_knife") + + for (new EquipType_e: section; section < EquipType_e; section++) { + Player_GiveWeapon(player, section) + } + + return HC_SUPERCEDE +} + +static stock InventorySlotType: GetWeaponSlot(const weaponId) { + // TODO: ReAPI update + static const weaponSlotInfo[] = {0,2,0,1,4,1,5,1,1,4,2,2,1,1,1,1,2,2,1,1,1,1,1,1,1,4,2,1,1,3,1} + + if (weaponId < 0 || weaponId >= sizeof(weaponSlotInfo)) + return NONE_SLOT + + return InventorySlotType: weaponSlotInfo[weaponId] +} + +public CBasePlayer_GiveNamedItem(const player, const weaponName[]) { + // TODO: doesn't work now, need ReGameDLL fixes + /* + if (!Player_HasChosenEquip(player)) + return HC_CONTINUE + + new weaponId = rg_get_weapon_info(weaponName, WI_ID) + new EquipType_e: receiveWeaponSlot = EquipType_e: (GetWeaponSlot(weaponId) - InventorySlotType: 1) + + if (receiveWeaponSlot < et_Primary || receiveWeaponSlot >= EquipType_e) + return HC_CONTINUE + + if (g_playerWeapons[player][receiveWeaponSlot] != EQUIP_NOT_CHOOSEN) { + Player_GiveWeapon(player, receiveWeaponSlot) + return HC_SUPERCEDE + } + + return HC_CONTINUE + */ +} + +public EquipManager_PlayerKilled(const victim, const killer) { + if (!is_user_alive(killer)) + return + + new item = get_member(killer, m_pActiveItem) + if (!is_entity(item)) + return + + Player_SaveActiveSlot(killer, item) + Player_SaveWeaponState(killer, item) +} + +static Player_SaveActiveSlot(const player, const item) { + new InventorySlotType: slot = InventorySlotType: (rg_get_iteminfo(item, ItemInfo_iSlot) + 1) + if (slot >= KNIFE_SLOT) + return + + g_lastWeaponSlot[player] = slot +} + +static Player_SaveWeaponState(const player, const item) { + g_weaponState[player] = get_member(item, m_Weapon_iWeaponState) +} + +public Player_ForceSlotChoose(const player) { + if (!is_user_alive(player)) + return + + new item = get_member(player, m_rgpPlayerItems, g_lastWeaponSlot[player]) + if (!is_entity(item)) + return + + new weaponName[32] + rg_get_weapon_info(get_member(item, m_iId), WI_NAME, weaponName, charsmax(weaponName)) + + // Restore activeItem + rg_internal_cmd(player, weaponName) + + // Restore weaponState + set_member(item, m_Weapon_iWeaponState, g_weaponState[player]) +} + +static bool: Player_GiveWeapon(const player, const EquipType_e: section) { + if (g_playerWeapons[player][section] == EQUIP_NOT_CHOOSEN) + return false + + new weaponName[32] + ArrayGetString( + Array: g_equip[section], + g_playerWeapons[player][section], + weaponName, + charsmax(weaponName) + ) + + rg_give_item(player, weaponName, .type = GT_REPLACE) + return true +} + +static bool: Player_HasChosenEquip(const player) { + for (new EquipType_e: section; section < EquipType_e; section++) { + if (g_playerWeapons[player][section] != EQUIP_NOT_CHOOSEN) { + return true + } + } + + return false +} + +EquipManager_PlayerResetEquip(const player) { + for (new EquipType_e: section; section < EquipType_e; section++) { + g_playerWeapons[player][section] = EQUIP_NOT_CHOOSEN + } +} + +static bool: HasEquipItems(const EquipType_e: section) { + return ArraySize(Array: g_equip[section]) != 0 +} + +public ConCmd_redm_dump_equip(const player) { + DumpEquip(player) + + return PLUGIN_HANDLED +} + +static DumpEquip(const player = 0) { + if (Array: g_equip[et_Primary] == Invalid_Array && Array: g_equip[et_Secondary] == Invalid_Array) { + console_print(player, "DumpEquip(): WARNING! Equip not itinialized!") + return + } + + console_print(player, "^nDump current equipset:") + console_print(player, "| %-2s | %-16s | %-16s |", + "#", "Primary", "Secondary" + ) + + new const template[] = "| %-2i | %-16s | %-16s |" + console_print(player, "| -- | ---------------- | ---------------- |") + + new primaryCount = ArraySize(Array: g_equip[et_Primary]) + new secondaryCount = ArraySize(Array: g_equip[et_Secondary]) + new size = max(primaryCount, secondaryCount) + + for (new i; i < size; i++) { + new primaryWeaponName[32], secondaryWeaponName[32] + + if (i < primaryCount) + ArrayGetString(Array: g_equip[et_Primary], i, primaryWeaponName, charsmax(primaryWeaponName)) + + if (i < secondaryCount) + ArrayGetString(Array: g_equip[et_Secondary], i, secondaryWeaponName, charsmax(secondaryWeaponName)) + + console_print( + player, + template, + i + 1, + primaryWeaponName, + secondaryWeaponName + ) + } + + console_print(player, "^n") +} \ No newline at end of file diff --git a/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_features.inc b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_features.inc new file mode 100644 index 0000000..ffa68d2 --- /dev/null +++ b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_features.inc @@ -0,0 +1,253 @@ +static g_fwdPrecacheEvent = -1 +static g_gunsEventsId +static g_oldGroupinfo[MAX_PLAYERS + 1] + +static Float: redm_sounds_distance +static Float: redm_healer +static Float: redm_healer_hs +static redm_fade +static redm_refill_ammo +static redm_hitsound +static mp_damage_headshot_only +static bool: redm_hide_other_deathnotice + + +Features_Precache() { + g_fwdPrecacheEvent = register_forward(FM_PrecacheEvent , "PrecacheEvent_Post", ._post = true) +} + +Features_Init() { + register_message(get_user_msgid("HudTextArgs"), "MsgHook_HudTextArgs") + register_message(get_user_msgid("DeathMsg"), "MsgHook_DeathMsg") + + unregister_forward(FM_PrecacheEvent, g_fwdPrecacheEvent, .post = true) + register_forward(FM_PlaybackEvent, "PlaybackEvent") + register_forward(FM_PlaybackEvent, "PlaybackEvent_Post", ._post = true) + + RegisterHam(Ham_Touch, "game_player_equip", "BlockMapAutoEquipEntites", .Post = false) + RegisterHam(Ham_Use, "game_player_equip", "BlockMapAutoEquipEntites", .Post = false) + RegisterHam(Ham_Use, "player_weaponstrip", "BlockMapAutoEquipEntites", .Post = false) + + RegisterHookChain(RG_CBasePlayer_TakeDamage, "CBasePlayer_TakeDamage_Post", .post = true) + RegisterHookChain(RG_CBasePlayer_TraceAttack, "CBasePlayer_TraceAttack", .post = false) + // RegisterHookChain(RH_SV_StartSound, "SV_StartSound", .post = false) + + bind_pcvar_float(create_cvar("redm_healer", "10.0"), redm_healer) + bind_pcvar_float(create_cvar("redm_healer_hs", "20.0"), redm_healer_hs) + bind_pcvar_float(create_cvar("redm_sounds_distance", "1500.0"), redm_sounds_distance) + bind_pcvar_num(create_cvar("redm_fade", "1"), redm_fade) + bind_pcvar_num(create_cvar("redm_refill_ammo", "1"), redm_refill_ammo) + bind_pcvar_num(create_cvar("redm_hitsound", "1"), redm_hitsound) + bind_pcvar_num(create_cvar("mp_damage_headshot_only", "0"), mp_damage_headshot_only) + bind_pcvar_num(create_cvar("redm_hide_other_deathnotice", "1"), redm_hide_other_deathnotice) +} + +public MsgHook_HudTextArgs() { + return IsActive() ? PLUGIN_HANDLED : PLUGIN_CONTINUE +} + +public MsgHook_DeathMsg(msgid, dest, receiver) { + enum { arg_killer = 1, arg_victim, arg_headshot, arg_weapon_name } + + if (!IsActive()) + return PLUGIN_CONTINUE + + if (!redm_hide_other_deathnotice) + return PLUGIN_CONTINUE + + new killer = get_msg_arg_int(arg_killer) + new victim = get_msg_arg_int(arg_victim) + new headshot = get_msg_arg_int(arg_headshot) + new killerWeaponName[64] + get_msg_arg_string(arg_weapon_name, killerWeaponName, charsmax(killerWeaponName)) + + for(new p = 1; p <= MaxClients; p++) { + // check player settings + + if(p != killer && p != victim) + continue + + if(!is_user_connected(p) || is_user_bot(p)) + continue + + UTIL_DeathMsg(MSG_ONE, p, killer, victim, headshot, killerWeaponName) + } + + return PLUGIN_HANDLED +} + + +public PrecacheEvent_Post(type, const name[]) { + new const gunsEvents[][] = { + "events/awp.sc", "events/g3sg1.sc", "events/ak47.sc", + "events/scout.sc", "events/m249.sc", "events/m4a1.sc", + "events/sg552.sc", "events/aug.sc", "events/sg550.sc", + "events/m3.sc", "events/xm1014.sc", "events/usp.sc", + "events/mac10.sc", "events/ump45.sc", "events/fiveseven.sc", + "events/p90.sc", "events/deagle.sc", "events/p228.sc", + "events/glock18.sc", "events/mp5n.sc", "events/tmp.sc", + "events/elite_left.sc", "events/elite_right.sc", "events/galil.sc", + "events/famas.sc" + } + + for (new i = 0; i < sizeof(gunsEvents); i++) { + if (strcmp(gunsEvents[i], name)) + continue + + g_gunsEventsId |= (1 << get_orig_retval()) + return FMRES_HANDLED + } + + return FMRES_IGNORED +} + + +public PlaybackEvent(flags, invoker, eventid, Float: delay, Float: Origin[3], Float: Angles[3], Float: fparam1, Float: fparam2, iparam1, iparam2, bparam1, bparam2) { + if (!IsActive()) + return + + if (redm_sounds_distance <= 0.0) + return + + if (!IsGunsEvent(eventid)) + return + + if (invoker < 1 || invoker > MaxClients) + return + + g_oldGroupinfo[invoker] = pev(invoker, pev_groupinfo) + + set_pev(invoker, pev_groupinfo, 1) + + for (new i = 1; i <= MaxClients; i++) { + if (i == invoker) + continue + + if (fm_entity_range(i, invoker) < redm_sounds_distance) + continue + + g_oldGroupinfo[i] = pev(i, pev_groupinfo) + set_pev(i, pev_groupinfo, 2) + } +} + +public PlaybackEvent_Post(flags, invoker, eventid, Float: delay, Float: Origin[3], Float: Angles[3], Float: fparam1, Float: fparam2, iparam1, iparam2, bparam1, bparam2) { + if (!IsActive()) + return + + if (redm_sounds_distance <= 0.0) + return + + if (!IsGunsEvent(eventid)) + return + + if (invoker < 1 || invoker > MaxClients) + return + + // TODO: refactor that shit + set_pev(invoker, pev_groupinfo, g_oldGroupinfo[invoker]) + + for (new i = 1; i <= MaxClients; i++) { + if (i == invoker) + continue + + if (fm_entity_range(i, invoker) < redm_sounds_distance) + continue + + set_pev(i, pev_groupinfo, g_oldGroupinfo[i]) + } +} + +static bool: IsGunsEvent(const eventId) { + return bool: (g_gunsEventsId & (1 << eventId)) +} + +public BlockMapAutoEquipEntites() { + return IsActive() ? HAM_SUPERCEDE : HAM_IGNORED +} + +public CBasePlayer_TakeDamage_Post(const victim, const inflictor, const attacker, const Float: damage, const bitsDamageType) { + if (!IsActive()) + return + + if (!attacker || attacker == victim) + return + + if (!rg_is_player_can_takedamage(victim, attacker)) + return + + if (is_user_bot(attacker)) + return + + if (redm_hitsound) { + UTIL_PlaySoundEffect( + attacker, + "buttons/bell1.wav", + 0.6, + get_member(victim, m_LastHitGroup) == HITGROUP_HEAD ? 240 : 200 + ) + } +} + +public Features_PlayerKilled(const victim, const killer) { + if (redm_healer + redm_healer_hs > 0.0) { + new bool: isHeadshot = bool: get_member(victim, m_bHeadshotKilled) + ExecuteHamB(Ham_TakeHealth, killer, isHeadshot ? redm_healer_hs : redm_healer, DMG_GENERIC) + } + + if (redm_refill_ammo) { + UTIL_ReloadWeapons(killer, .currentWeapon = (redm_refill_ammo == 1)) + } + + if (is_user_bot(killer)) + return + + if (redm_fade) { + UTIL_ScreenFade(killer) + } +} + +public CBasePlayer_TraceAttack(victim, attacker, Float: damage, Float: vecDir[3], tracehandle, bitsDamageType) { + if (!IsActive()) + return HC_CONTINUE + + if (!mp_damage_headshot_only) + return HC_CONTINUE + + if (!(bitsDamageType & DMG_BULLET)) + return HC_CONTINUE + + if (GetCurrentWeapon(attacker) == WEAPON_KNIFE) + return HC_CONTINUE + + new bool: hitHead = get_tr2(tracehandle, TR_iHitgroup) == HIT_HEAD + return hitHead ? HC_CONTINUE : HC_SUPERCEDE +} + +stock WeaponIdType: GetCurrentWeapon(const player) { + new activeItem = get_member(player, m_pActiveItem) + if (!activeItem) + return WEAPON_NONE + + return WeaponIdType: get_member(activeItem, m_iId) +} + +public SV_StartSound(const recipients, const entity, const channel, const sample[], const volume, Float:attenuation, const fFlags, const pitch) { + /* server_print("s:`%s`, r:%i, att:%.2f, pitch:%i", + sample, recipients, attenuation, pitch + ) */ +} + +static stock UTIL_DeathMsg(const dest, const receiver, const killer, const victim, const headshot, const weaponName[]) { + static msg_deathMsg + if(!msg_deathMsg) + msg_deathMsg = get_user_msgid("DeathMsg") + + message_begin(dest, msg_deathMsg, _, receiver) + write_byte(killer) + write_byte(victim) + write_byte(headshot) + write_string(weaponName) + message_end() +} + diff --git a/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_round_modes.inc b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_round_modes.inc new file mode 100644 index 0000000..04de214 --- /dev/null +++ b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_round_modes.inc @@ -0,0 +1,167 @@ +static g_currentRoundModeIdx = -1 +static redm_modes_switch[32] + +RoundModes_Init() { + RegisterHookChain(RG_RoundEnd, "RoundEnd", .post = false) + RegisterHookChain(RG_CSGameRules_RestartRound, "CSGameRules_RestartRound", .post = false) + RegisterHookChain(RG_CBasePlayer_OnSpawnEquip, "CBasePlayer_OnSpawnEquip", .post = false) + + bind_pcvar_string( + create_cvar("redm_modes_switch", "sequentially"), + redm_modes_switch, + charsmax(redm_modes_switch) + ) +} + +public RoundEnd(WinStatus: status, ScenarioEventEndRound: event, Float: tmDelay) { + if (strcmp(redm_modes_switch, "disable") == 0) + return + + new nextModeIdx = GetNextMode() + if (nextModeIdx == -1) + return + + // TODO: + // SetHookChainArg(1, ATYPE_INTEGER, WINSTATUS_NONE) + // SetHookChainArg(2, ATYPE_INTEGER, ROUND_TARGET_SAVED) + + new modeName[32] + GetModeInfo(nextModeIdx, modeName, charsmax(modeName)) + + client_print_color(0, print_team_red, "[Re:DM] Next mode is `^3%s^1`", + modeName + ) + + g_currentRoundModeIdx = nextModeIdx +} + +public CSGameRules_RestartRound() { + if (g_currentRoundModeIdx == -1) + return + + new modeName[32] + if (!GetModeInfo(g_currentRoundModeIdx, modeName, charsmax(modeName))) + return + + ApplyMode(g_currentRoundModeIdx) + + for (new p = 1; p <= MaxClients; p++) { + EquipManager_PlayerResetEquip(p) + } +} + +public CBasePlayer_OnSpawnEquip(const player, bool: addDefault, bool: equipGame) { + set_member(player, m_bNotKilled, false) + rg_set_user_armor(player, 0, ARMOR_NONE) + + if (g_currentRoundModeIdx == -1) + return + + new modeName[32] + GetModeInfo(g_currentRoundModeIdx, modeName, charsmax(modeName)) + + set_dhudmessage( + .red = 200, + .green = 200, + .blue = 200, + .y = 0.85, + .holdtime = 4.0 + ) + + SetGlobalTransTarget(player) + + new bool: isLang = (GetLangTransKey(modeName) != TransKey_Bad) + + show_dhudmessage( + player, + isLang ? "%l: %l" : "%l: %s", + "CurrentMode", + modeName + ) +} + + +static GetModeInfo(const index, name[], len) { + if (!json_object_has_value(Config_GetCurrent(), "modes")) + return false + + new JSON: objModes = json_object_get_value(Config_GetCurrent(), "modes") + + new count = json_object_get_count(objModes) + if (!count) { + return false + } + + if (index < 0 || index >= count) { + return false + } + + json_object_get_name(objModes, index, name, len) + + json_free(objModes) + + return true +} + +static GetNextMode() { + if (!json_object_has_value(Config_GetCurrent(), "modes")) + return -1 + + new JSON: objModes = json_object_get_value(Config_GetCurrent(), "modes") + + new count = json_object_get_count(objModes) + json_free(objModes) + + if (!count) + return -1 + + new currentIdx = g_currentRoundModeIdx + + if (strcmp(redm_modes_switch, "random") == 0) { + currentIdx = random_num(0, count - 1) + } else if (strcmp(redm_modes_switch, "sequentially") == 0) { + ++currentIdx + currentIdx %= count + } + + return currentIdx +} + + +bool: ApplyMode(const index) { + if (index < 0) + return false + + ReloadConfig() + + if (!json_object_has_value(Config_GetCurrent(), "modes")) + return false + + new JSON: objModes = json_object_get_value(Config_GetCurrent(), "modes") + + new count = json_object_get_count(objModes) + if (!count) + return false + + new JSON: objMode = json_object_get_value_at(objModes, index) + if (json_object_has_value(objMode, "cvars")) { + new JSON: objCvars = json_object_get_value(objMode, "cvars") + + CvarsHandler_LoadCvars(objCvars) + + json_free(objCvars) + } + + json_free(objMode) + json_free(objModes) + + return true +} + +RoundModes_GetCurrentMode() { + return g_currentRoundModeIdx +} + +RoundModes_ResetCurrentMode() { + g_currentRoundModeIdx = -1 +} diff --git a/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_spawn_manager.inc b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_spawn_manager.inc new file mode 100644 index 0000000..48ffbfe --- /dev/null +++ b/cstrike/addons/amxmodx/scripting/ReDeathmatch/ReDM_spawn_manager.inc @@ -0,0 +1,77 @@ +static Array: g_spawnsManager = Invalid_Array +static g_currentSpawnMethodIdx = -1 + +enum _: SpawnStyle_s { + ss_Name[32], + ss_Callback, +} + +static redm_spawn_preset[32] + + +SpawnManager_Init() { + bind_pcvar_string( + create_cvar("redm_spawn_preset", "preset"), + redm_spawn_preset, + charsmax(redm_spawn_preset) + ) + + RegisterHookChain(RG_CSGameRules_GetPlayerSpawnSpot, "CSGameRules_GetPlayerSpawnSpot", .post = false) + + redm_setstyle(redm_spawn_preset) +} + +public CSGameRules_GetPlayerSpawnSpot(const player) { + if (!IsActive()) + return HC_CONTINUE + + if (!SpawnManager_Spawn(player)) + return HC_CONTINUE + + SetHookChainReturn(ATYPE_INTEGER, 0) + return HC_SUPERCEDE +} + +SpawnManager_AddMethod(const name[], const callbackHandle) { + if (g_spawnsManager == Invalid_Array) { + g_spawnsManager = ArrayCreate(_: SpawnStyle_s) + } + + new spawnStyle[SpawnStyle_s] + copy(spawnStyle[ss_Name], charsmax(spawnStyle[ss_Name]), name) + spawnStyle[ss_Callback] = callbackHandle + + ArrayPushArray(g_spawnsManager, spawnStyle) + + return (ArraySize(g_spawnsManager) - 1) +} + +SpawnManager_Spawn(const player) { + if (g_currentSpawnMethodIdx == -1) + return false + + new spawnStyle[SpawnStyle_s] + ArrayGetArray( + g_spawnsManager, + g_currentSpawnMethodIdx, + spawnStyle, + sizeof(spawnStyle) + ) + + new bool: ret = false + ExecuteForward(spawnStyle[ss_Callback], ret, player) + + return ret +} + +Array: SpawnManager_Get() { + return g_spawnsManager +} + +SpawnManager_GetCurrentMethodIdx() { + return g_currentSpawnMethodIdx +} + +SpawnManager_SetCurrentMethodIdx(const idx) { + g_currentSpawnMethodIdx = idx +} diff --git a/cstrike/addons/amxmodx/scripting/include/redm.inc b/cstrike/addons/amxmodx/scripting/include/redm.inc new file mode 100644 index 0000000..69b88d3 --- /dev/null +++ b/cstrike/addons/amxmodx/scripting/include/redm.inc @@ -0,0 +1,173 @@ + +#if defined _redm_included + #endinput +#endif +#define _redm_included + + +new const VERSION[] = "%VERSION%" + +/** + * Returns whether Re:DM is active + * + * @return True if active, false overwise. + */ +native bool: redm_active() + +/** + * Sets Re:DM to active (do not set the cvar!) + * + * @return True if active, false overwise. + */ +native bool: redm_set_active(const bool: active) + +/** + * Getting config from main plugin. + * + * @return Config JSON handle, Invalid_JSON if error occurred. + */ +native JSON: redm_get_config() + +/** + * Getting equip config from main plugin. + * + * @return Config JSON handle, Invalid_JSON if error occurred. + */ +native JSON: redm_get_equip_config() + +/** + * Get the number of registered spawner styles. + * + * @return Number of styles. + */ +native redm_spawnstyles() + +/** + * Get a spawn style info by index (indices start at 0). + * + * @param style_index Style index. + * @param name Buffer to copy the name. + * @param maxlength Maximum size of buffer. + * + * @error If an invalid handle is provided + * an error will be thrown. + * + * @return Number of cells copied from buffer. + */ +native redm_styleinfo(const style_index, name[], const length) + +/** + * Adds a spawn style handler + */ +native redm_addstyle(const name[], const function[]) + +/** + * Sets the current spawn style handler by name. + * The handler registered to this name will be called after every spawn. + */ +native redm_setstyle(const name[]) + +/** + * Returns the current style id + */ +native redm_curstyle() + +/** + * Called before initialization start. + */ +forward ReDM_ChangeState(const bool: active) + +/** + * Called before initialization start. + */ +forward ReDM_InitStart() + +/** + * Called after fully initialized. + */ +forward ReDM_Initialized() + +/** + * + */ +forward ReDM_GetConfigName(config[], const len) + +/** + * + */ +forward ReDM_GetConfigPrefixName(config[], const len, const mapPrefix[]) + + + +enum LogLevel { Trace, Debug, Info, Warning, Error, Fatal } +stock LogMessageEx(const LogLevel: level = Info, const message[], any: ...) { + static const logLevelString[LogLevel][] = { "TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "FATAL" } + static buffer[4096] + vformat(buffer, sizeof buffer, message, 3) + format(buffer, sizeof buffer,"[%.2f][%s] %s", get_gametime(), logLevelString[level], buffer) + + if (level == Fatal) + set_fail_state("%s", buffer) + + server_print("%s", buffer) +} + +stock UTIL_PlaySoundEffect(const player, const sound[], const Float: vol = 0.7, const pitch = PITCH_NORM) { + rh_emit_sound2( + player, + player, + CHAN_VOICE, + sound, + .vol = vol, + .pitch = pitch + ) +} + +stock UTIL_ReloadWeapons(const player, bool: currentWeapon = false) { + if (!currentWeapon) { + rg_instant_reload_weapons(player) + return + } + + new activeItem = get_member(player, m_pActiveItem) + if (!is_nullent(activeItem)) { + rg_instant_reload_weapons(player, activeItem) + } +} + +stock bool: IsBlind(const player) { + return bool:(Float: get_member(player, m_blindUntilTime) > get_gametime()) +} + +stock FixedUnsigned16(Float: value, scale = (1 << 12)) { + return clamp(floatround(value * scale), 0, 0xFFFF) +} + +stock UTIL_ScreenFade(const player, const Float: fxTime = 0.2, const Float: holdTime = 0.2, const color[3] = {0, 200, 0}, const alpha = 50) { + if (IsBlind(player)) + return + + const FFADE_IN = 0x0000 + + static msgId_ScreenFade + if (!msgId_ScreenFade) { + msgId_ScreenFade = get_user_msgid("ScreenFade") + } + + message_begin(MSG_ONE_UNRELIABLE, msgId_ScreenFade, .player = player) + write_short(FixedUnsigned16(fxTime)) + write_short(FixedUnsigned16(holdTime)) + write_short(FFADE_IN) + write_byte(color[0]) + write_byte(color[1]) + write_byte(color[2]) + write_byte(alpha) + message_end() +} + +stock UTIL_DumpJSON(JSON: obj, msg[]) { + new buffer[1024] + json_serial_to_string(obj, buffer, charsmax(buffer)) + + server_print("%s:`%s`", msg, buffer) +} diff --git a/cstrike/addons/amxmodx/scripting/redm_spawns.sma b/cstrike/addons/amxmodx/scripting/redm_spawns.sma new file mode 100644 index 0000000..b49afcb --- /dev/null +++ b/cstrike/addons/amxmodx/scripting/redm_spawns.sma @@ -0,0 +1,951 @@ +#include +#include +#include +#include +#include + +#include +#include + + +static g_mapName[MAX_MAPNAME_LENGTH] + +static const g_spawnClassname[] = "view_spawn" + +static bool: g_editorEnabled = false + +enum HullVacant_s { + hull_Stand, + hull_Duck, + hull_Invalid, +} + +enum EditorProps_s { + ep_team, + ep_focusEntity, + ep_gravityPreset, + ep_group[32] +} + +static g_editorProps[MAX_PLAYERS + 1][EditorProps_s] + +new JSON: g_arrSpawns = Invalid_JSON + +static const g_spawnViewModels[_: TeamName - 1][] = { + "models/player/vip/vip.mdl", + "models/player/leet/leet.mdl", + "models/player/gign/gign.mdl", +} + +static const g_teamName[_: TeamName - 1][] = { + "ANY", + "T", + "CT", +} + +static Float: g_gravityValues[] = { + 1.0, 0.5, 0.25, 0.05 +} + +// Determines whether players are to spawn. 0 = default; 1 = both teams; 2 = Terrorists; 3 = CTs. +new mp_randomspawn = 1 + +// If non-zero, a randomly spawning player will, if possible, not be spawned at a spawn point with direct line of sight to another player. (2 = check viewcone) +new mp_randomspawn_los = 2 + +// If greater than 0, a randomly spawning player will, if possible, not be spawned at a spawn point where the smallest distance to another player is smaller than the value of this ConVar. +new Float: mp_randomspawn_dist = 1500.0 + +new bool: mp_freeforall + + +public plugin_precache() { + for (new i = 0; i < sizeof(g_spawnViewModels); i++) { + precache_model(g_spawnViewModels[i]) + } +} + +public plugin_init() { + register_plugin("Spawns manager", VERSION, "Sergey Shorokhov") + register_dictionary("common.txt") + + get_mapname(g_mapName, charsmax(g_mapName)) + GameDLLSpawnsCountFix() + + register_clcmd("enter_spawnGroup", "ClCmd_EnterSpawnGroup") + + redm_addstyle("preset", "SpawnPreset_DefaultPreset") +} + +public plugin_cfg() { + bind_pcvar_num(get_cvar_pointer("mp_freeforall"), mp_freeforall) + // set_pcvar_bounds(get_cvar_pointer("mp_forcerespawn"), CvarBound_Lower, true, 0.1) // TODO + + bind_pcvar_num(create_cvar("mp_randomspawn", "1"), mp_randomspawn) + bind_pcvar_num(create_cvar("mp_randomspawn_los", "2"), mp_randomspawn_los) + bind_pcvar_float(create_cvar("mp_randomspawn_dist", "1500.0"), mp_randomspawn_dist) + + RegisterHookChain(RG_CBasePlayer_UseEmpty, "CBasePlayer_UseEmpty", .post = false) + + register_concmd("redm_edit_spawns", "ConCmd_EditSpawns", + ADMIN_MAP, + "Edits spawn configuration" + ) + + register_concmd("redm_convert_spawns", "ConCmd_ConvertOldSpawns", + ADMIN_MAP, + "Convert old spawns to new format" + ) + + Editor_ReloadSpawns() +} + +public client_putinserver(player) { + Editor_ResetProps(player) +} + +public ConCmd_EditSpawns(const player, const level, const cid) { + SetGlobalTransTarget(player) + + if (!cmd_access(player, level, cid, 1)) + return PLUGIN_HANDLED + + g_editorEnabled = !g_editorEnabled + + if (!g_editorEnabled) { + Editor_Disable(player) + } else { + Editor_Enable(player) + } + + console_print(player, " * Spawns editor `%s`", + g_editorEnabled ? "enabled" : "disabled" + ) + + return PLUGIN_HANDLED +} + +public ConCmd_ConvertOldSpawns(const player, const level, const cid) { + SetGlobalTransTarget(player) + + if (!cmd_access(player, level, cid, 1)) + return PLUGIN_HANDLED + + Editor_ConvertSpawns() + + return PLUGIN_HANDLED +} + + +static Editor_Enable(const player) { + if (g_arrSpawns != Invalid_JSON) + Editor_AddViewSpawns(g_arrSpawns) + + Menu_Editor(player) +} + +static Editor_Disable(const player) { + reset_menu(player) + + Editor_ResetProps(player) + + Editor_SaveSpawns() + Editor_RemoveViewSpawns() + + Editor_ReloadSpawns() +} + +static Editor_ReloadSpawns() { + if (g_arrSpawns != Invalid_JSON) + json_free(g_arrSpawns) + + g_arrSpawns = Editor_LoadSpawns() +} + +static Editor_ResetProps(const player) { + g_editorProps[player][ep_focusEntity] = FM_NULLENT + g_editorProps[player][ep_team] = 0 + g_editorProps[player][ep_gravityPreset] = 0 + g_editorProps[player][ep_group][0] = EOS +} + +public CBasePlayer_UseEmpty(const player) { + if (!g_editorEnabled) + return HC_CONTINUE + + Editor_Focus(player) + Menu_Editor(player) + + return HC_SUPERCEDE +} + +static Editor_Focus(const player) { + new entity = FindEntityByAim(player) + if (entity != FM_NULLENT) { + if (g_editorProps[player][ep_focusEntity] != FM_NULLENT) + Editor_ClearEntityFocus(player) + + Editor_SetEntityFocus(player, entity) + + return + } + + entity = g_editorProps[player][ep_focusEntity] + if (entity != FM_NULLENT) { + Editor_ClearEntityFocus(player) + } +} + +FindEntityByAim(const player) { + new entity = FM_NULLENT + + SetEntitysSolid(true) + get_user_aiming(player, entity, .dist = 1000) + SetEntitysSolid(false) + + if (fm_is_ent_classname(entity, g_spawnClassname)) + return entity + + return FM_NULLENT +} + +SetEntitysSolid(const bool: solid) { + new entity = NULLENT + while ((entity = fm_find_ent_by_class(entity, g_spawnClassname))) { + if (!solid) { + engfunc(EngFunc_SetSize, entity, Float: {0.0, 0.0, 0.0}, Float: {0.0, 0.0, 0.0}) + } else { + engfunc(EngFunc_SetSize, entity, Float: {-16.0, -16.0, -36.0}, Float: {16.0, 16.0, 36.0}) + } + } +} + +static bool: Editor_SetEntityFocus(const player, const entity = FM_NULLENT) { + if (entity == FM_NULLENT) + return false + + static const teamColors[_: TeamName - 1][] = { + { 255, 255, 255 }, + { 255, 0, 0 }, + { 0, 0, 255 }, + } + + new bool: inDuck = bool: (pev(entity, pev_flags) & FL_DUCKING) + fm_animate_entity(entity, inDuck ? ACT_FLY : ACT_RUN, 0.3) + new team = pev(entity, pev_team) + fm_set_rendering( + entity, + kRenderFxGlowShell, + teamColors[team][0], + teamColors[team][1], + teamColors[team][2], + .amount = 20 + ) + + g_editorProps[player][ep_focusEntity] = entity + g_editorProps[player][ep_team] = pev(entity, pev_team) + + pev(entity, pev_netname, + g_editorProps[player][ep_group], + charsmax(g_editorProps[][ep_group]) + ) + + return true +} + +static Editor_ClearEntityFocus(const player) { + new entity = g_editorProps[player][ep_focusEntity] + new bool: inDuck = bool: (pev(entity, pev_flags) & FL_DUCKING) + + fm_animate_entity(entity, inDuck ? ACT_CROUCHIDLE : ACT_IDLE) + fm_set_rendering(entity) + + g_editorProps[player][ep_focusEntity] = FM_NULLENT + g_editorProps[player][ep_group][0] = EOS +} + +static Menu_Editor(const player/* , const level */) { + SetGlobalTransTarget(player) + + if (!is_user_alive(player)) + return + + static callback + if (!callback) + callback = menu_makecallback("MenuCallback_Editor") + + new menu = menu_create("Spawns manager", "MenuHandler_Editor") + + menu_additem( + menu, + (g_editorProps[player][ep_focusEntity] != FM_NULLENT) ? "Update" : "Add", + .callback = callback + ) + + new team = g_editorProps[player][ep_team] + menu_additem(menu, fmt("Team: %s", g_teamName[team])) + menu_additem(menu, "Teleport", .callback = callback) + menu_additem(menu, "Delete", .callback = callback) + new gravityPreset = g_editorProps[player][ep_gravityPreset] + menu_additem(menu, fmt("Gravity: %.2f", g_gravityValues[gravityPreset]), .callback = callback) + menu_additem(menu, fmt("Group: %s", g_editorProps[player][ep_group]), .callback = callback) + + new stats[_: TeamName - 1] + new total = Editor_GetStats(stats) + + menu_addtext(menu, + fmt("^nTotal:%i (ANY:%i, T:%i, CT:%i)", + total, stats[0], stats[1], stats[2] + ), + .slot = false + ) + + menu_display(player, menu) +} + +public MenuCallback_Editor(const player, const menu, const item) { + switch(item) { + case 2, 3: { + if (g_editorProps[player][ep_focusEntity] == FM_NULLENT) + return ITEM_DISABLED + } + } + + return ITEM_IGNORE +} + +public MenuHandler_Editor(const player, const menu, const item) { + menu_destroy(menu) + + if (item < 0) { + return PLUGIN_HANDLED + } + + switch (item) { + case 0: { + new entity = g_editorProps[player][ep_focusEntity] + if (entity != FM_NULLENT) { + Spawn_Update(player, entity) + } else { + Spawn_Add(player) + } + } + case 1: { + ++g_editorProps[player][ep_team] + g_editorProps[player][ep_team] %= sizeof(g_spawnViewModels) + + new entity = g_editorProps[player][ep_focusEntity] + if (entity != FM_NULLENT) + Spawn_SetTeam(player, entity, g_editorProps[player][ep_team]) + } + case 2: { + new entity = g_editorProps[player][ep_focusEntity] + + new Float: origin[3], Float: angle[3], Float: vAngle[3] + Spawn_EntityGetPosition(entity, origin, angle, vAngle) + Spawn_EntitySetPosition(player, origin, angle, vAngle) + } + case 3: { + new entity = g_editorProps[player][ep_focusEntity] + + Spawn_Delete(entity) + g_editorProps[player][ep_focusEntity] = FM_NULLENT + } + case 4: { + ++g_editorProps[player][ep_gravityPreset] + g_editorProps[player][ep_gravityPreset] %= sizeof(g_gravityValues) + + set_pev( + player, + pev_gravity, + g_gravityValues[g_editorProps[player][ep_gravityPreset]] + ) + } + case 5: { + client_cmd(player, "messagemode enter_spawnGroup") + } + } + + Menu_Editor(player) + + return PLUGIN_HANDLED +} + +public ClCmd_EnterSpawnGroup(const player, const level, const cid) { + if (!cmd_access(player, level, cid, 1)) + return PLUGIN_HANDLED + + static spawnGroup[32] + read_argv(1, spawnGroup, charsmax(spawnGroup)) + + if (!spawnGroup[0]) { + Menu_Editor(player) + + return PLUGIN_HANDLED + } + + copy(g_editorProps[player][ep_group], charsmax(g_editorProps[][ep_group]), spawnGroup) + + new entity = g_editorProps[player][ep_focusEntity] + if (entity != FM_NULLENT) + set_pev(entity, pev_netname, spawnGroup) + + Menu_Editor(player) + return PLUGIN_HANDLED +} + +static Editor_GetStats(stats[_: TeamName - 1]) { + new total + new entity = NULLENT + while ((entity = fm_find_ent_by_class(entity, g_spawnClassname))) { + new team = pev(entity, pev_team) + switch(team) { + case 1, 2: ++stats[team] + default: ++stats[0] + } + + ++total + } + + return total +} + +static bool: Spawn_Add(const player) { + new Float: origin[3], Float: angle[3], Float: vAngle[3] + Spawn_EntityGetPosition(player, origin, angle, vAngle) + origin[2] += 0.1 + + new team = g_editorProps[player][ep_team] + + return Add(origin, angle, vAngle, team, g_editorProps[player][ep_group]) +} + +static bool: Add(const Float: origin[3], const Float: angle[3], const Float: vAngle[3], const team, const group[32]) { + new entity = Spawn_CreateEntity() + if (!Spawn_EntitySetPosition(entity, origin, angle, vAngle)) { + Spawn_Delete(entity) + return false + } + + set_pev(entity, pev_team, team) + set_pev(entity, pev_netname, group) + engfunc(EngFunc_SetModel, g_spawnViewModels[team]) + + return true +} + +static Spawn_Update(const player, const entity) { + new Float: origin[3], Float: angle[3], Float: vAngle[3] + Spawn_EntityGetPosition(player, origin, angle, vAngle) + + Spawn_EntitySetPosition(entity, origin, angle, vAngle) + Editor_SetEntityFocus(player, entity) +} + +static Spawn_Delete(const entity) { + engfunc(EngFunc_RemoveEntity, entity) +} + +static Spawn_SetTeam(const player, const entity, const team) { + set_pev(entity, pev_team, team) + engfunc(EngFunc_SetModel, g_spawnViewModels[team]) + + Editor_SetEntityFocus(player, entity) +} + +static Spawn_EntityGetPosition(const entity, Float: origin[3], Float: angle[3], Float: vAngle[3]) { + pev(entity, pev_origin, origin) + pev(entity, pev_angles, angle) + pev(entity, pev_v_angle, vAngle) +} + +static bool: Spawn_EntitySetPosition(const entity, const Float: origin[3], const Float: angle[3], const Float: vAngle[3]) { + new HullVacant_s: res = CheckHullVacant(origin, .ignorePlayers = g_editorEnabled) + if (res == hull_Invalid) { + if (g_editorEnabled) + client_print_color(is_user_connected(entity) ? entity : 0, print_team_red, "^3Invalid place!^1") + + return false + } + + engfunc(EngFunc_SetOrigin, entity, origin) + set_pev(entity, pev_v_angle, vAngle) + set_pev(entity, pev_angles, is_user_alive(entity) ? vAngle : angle) + set_pev(entity, pev_fixangle, 1) + set_pev(entity, pev_velocity, Float: {0.0, 0.0, 0.0}) + set_pev(entity, pev_punchangle, Float: {0.0, 0.0, 0.0}) + set_pev(entity, pev_avelocity, Float: {0.0, 0.0, 0.0}) + + if (res == hull_Duck) { + set_pev(entity, pev_flags, pev(entity, pev_flags) | FL_DUCKING) + } else { + set_pev(entity, pev_flags, pev(entity, pev_flags) & ~FL_DUCKING) + } + + fm_animate_entity(entity, (res == hull_Duck) ? ACT_CROUCHIDLE : ACT_IDLE) + + return true +} + +static Spawn_CreateEntity() { + new entity = fm_create_entity("info_target") + + set_pev(entity, pev_classname, g_spawnClassname) + set_pev(entity, pev_solid, SOLID_BBOX) + + return entity +} + +static stock fm_animate_entity(const entity, const Activity: sequence = ACT_IDLE, const Float: framerate = 0.0) { + set_pev(entity, pev_sequence, sequence) + set_pev(entity, pev_framerate, framerate) +} + +stock HullVacant_s: CheckHullVacant(const Float: origin[3], const bool: ignorePlayers = false) { + static hulls[] = { HULL_HUMAN, HULL_HEAD } + + new res + for (new i; i < sizeof(hulls); i++) { + engfunc(EngFunc_TraceHull, origin, origin, ignorePlayers ? IGNORE_MONSTERS : DONT_IGNORE_MONSTERS, hulls[i], 0, 0) + + if (!get_tr2(0, TR_StartSolid) /*&& !get_tr2(0, TR_AllSolid) && get_tr2(0, TR_InOpen) */) { + return HullVacant_s: res + } + ++res + } + + return HullVacant_s: res +} + +static Editor_RemoveViewSpawns() { + new entity = FM_NULLENT + while ((entity = fm_find_ent_by_class(entity, g_spawnClassname))) { + engfunc(EngFunc_RemoveEntity, entity) + } +} + +static Editor_AddViewSpawns(const JSON: arrSpawns) { + if (arrSpawns == Invalid_JSON) { + LogMessageEx(Debug, "Editor_AddViewSpawns: `arrSpawns` is inavlid!") + return + } + + for (new idx, size = json_array_get_count(arrSpawns); idx < size; idx++) { + new JSON: spawn = json_array_get_value(arrSpawns, idx) + + new Float: origin[3], Float: angle[3], Float: vAngle[3] + new team = json_object_get_number(spawn, "team") + new JSON: arrOrigin = json_object_get_value(spawn, "origin") + new JSON: arrAngle = json_object_get_value(spawn, "angle") + new JSON: arrVAngle = json_object_get_value(spawn, "vAngle") + + new group[32] + json_object_get_string(spawn, "group", group, charsmax(group)) + + for (new i; i < sizeof(origin); i++) { + origin[i] = json_array_get_real(arrOrigin, i) + if (i < 2) { + angle[i] = json_array_get_real(arrAngle, i) + vAngle[i] = json_array_get_real(arrVAngle, i) + } + } + + if (!Add(origin, angle, vAngle, team, group)) { + LogMessageEx(Warning, "Editor_AddViewSpawns: Can't add spawn `%i`!") + } + + json_free(arrOrigin) + json_free(arrAngle) + json_free(arrVAngle) + + json_free(spawn) + } +} + +static Editor_SaveSpawns() { + new JSON: arrSpawns = json_init_array() + + new entity = FM_NULLENT + while ((entity = fm_find_ent_by_class(entity, g_spawnClassname))) { + new JSON: spawn = json_init_object() + + new Float: origin[3], Float: angle[3], Float: vAngle[3] + pev(entity, pev_origin, origin) + pev(entity, pev_angles, angle) + pev(entity, pev_v_angle, vAngle) + new team = pev(entity, pev_team) + new group[32] + pev(entity, pev_netname, group, charsmax(group)) + + json_object_set_number(spawn, "team", team) + json_object_set_string(spawn, "group", group) + + new JSON: arrOrigin = json_init_array() + new JSON: arrAngle = json_init_array() + new JSON: arrVAngle = json_init_array() + + for (new i; i < 3; i++) { + json_array_append_real(arrOrigin, origin[i]) + if (i < 2) { + json_array_append_real(arrAngle, angle[i]) + json_array_append_real(arrVAngle, vAngle[i]) + } + } + + json_object_set_value(spawn, "origin", arrOrigin) + json_object_set_value(spawn, "angle", arrAngle) + json_object_set_value(spawn, "vAngle", arrVAngle) + + json_free(arrOrigin) + json_free(arrAngle) + json_free(arrVAngle) + + json_array_append_value(arrSpawns, spawn) + + json_free(spawn) + } + + new JSON: objSpawns = json_init_object() + json_object_set_value(objSpawns, "spawns", arrSpawns) + json_free(arrSpawns) + + new filePath[PLATFORM_MAX_PATH] + get_datadir(filePath, charsmax(filePath)) + + formatex(filePath, charsmax(filePath), "%s/redm", filePath) + if (!dir_exists(filePath) && mkdir(filePath) == -1) + set_fail_state("Can't create folder `%s`", filePath) + + formatex(filePath, charsmax(filePath), "%s/%s.spawns.json", + filePath, g_mapName + ) + + if (!json_serial_to_file(objSpawns, filePath, true)) + set_fail_state("Can't create file `%s`", filePath) + + json_free(objSpawns) +} + +static JSON: Editor_LoadSpawns() { + new filePath[PLATFORM_MAX_PATH] + get_datadir(filePath, charsmax(filePath)) + + formatex(filePath, charsmax(filePath), "%s/redm/%s.spawns.json", + filePath, g_mapName + ) + + if (!file_exists(filePath)) { + LogMessageEx(Debug, "Editor_LoadSpawns: No spawns file found `%s`.", filePath) + return Invalid_JSON + } + + new JSON: objSpawns = json_parse(filePath, true, true) + if (objSpawns == Invalid_JSON) { + LogMessageEx(Debug, "Editor_LoadSpawns: Can't parse JSON file from `%s`.", filePath) + return Invalid_JSON + } + + new JSON: arrSpawns = json_object_get_value(objSpawns, "spawns") + + LogMessageEx(Debug, "Editor_LoadSpawns: Map `%s` total spawns: %i loaded.", + g_mapName, json_array_get_count(arrSpawns) + ) + + return arrSpawns +} + +static Editor_ConvertSpawns() { + new configsDir[PLATFORM_MAX_PATH] + get_configsdir(configsDir, charsmax(configsDir)) + + new csdmSpawnsDir[PLATFORM_MAX_PATH] + formatex(csdmSpawnsDir, charsmax(csdmSpawnsDir), + "%s/csdm/spawns", + configsDir + ) + + if (!dir_exists(csdmSpawnsDir)) { + LogMessageEx(Warning, "Editor_ConvertSpawns: Directorory not exists `%s`.", csdmSpawnsDir) + + return false + } + + new fileName[MAX_MAPNAME_LENGTH] + new dir = open_dir(csdmSpawnsDir, fileName, charsmax(fileName)) + if (!dir) { + LogMessageEx(Warning, "Editor_ConvertSpawns: Can't open directorory `%s`.", csdmSpawnsDir) + + return false + } + + new count + while (next_file(dir, fileName, charsmax(fileName))) { + new bool: isSpawnsFile = contain(fileName, ".spawns.cfg") != -1 + if (!isSpawnsFile) + continue + + ConvertOldSpawnsFile(fmt("%s/%s", csdmSpawnsDir, fileName)) + ++count + } + + LogMessageEx(Info, "Editor_ConvertSpawns: Succefully convert `%i` old spawn files.", count) + + close_dir(dir) + + return true +} + +static bool: ConvertOldSpawnsFile(const file[]) { + new fileName[PLATFORM_MAX_PATH] + remove_filepath(file, fileName, charsmax(fileName)) + + new map[PLATFORM_MAX_PATH] + split_string(fileName, ".spawns.cfg", map, charsmax(map)) + + new f = fopen(file, "r") + if (!f) { + LogMessageEx(Info, "ConvertOldSpawnsFile: Can't open file `%s`.", file) + + return false + } + + new JSON: arrSpawns = json_init_array() + while (!feof(f)) { + new data[1024] + fgets(f, data, charsmax(data)) + trim(data) + + new strOrigin[3][8], strAngles[3][8], strTeam[3], strVAngles[3][8] + new argC = parse( + data, + strOrigin[0], charsmax(strOrigin[]), + strOrigin[1], charsmax(strOrigin[]), + strOrigin[2], charsmax(strOrigin[]), + strAngles[0], charsmax(strAngles[]), + strAngles[1], charsmax(strAngles[]), + strAngles[2], charsmax(strAngles[]), + strTeam, charsmax(strTeam), + strVAngles[0], charsmax(strVAngles[]), + strVAngles[1], charsmax(strVAngles[]), + strVAngles[2], charsmax(strVAngles[]) + ) + + if (argC != 10) + continue + + { + new JSON: spawn = json_init_object() + + json_object_set_number(spawn, "team", strtol(strTeam)) + + new JSON: arrOrigin = json_init_array() + new JSON: arrAngle = json_init_array() + new JSON: arrVAngle = json_init_array() + + for (new i; i < 3; i++) { + json_array_append_real(arrOrigin, strtof(strOrigin[i])) + if (i < 2) { + json_array_append_real(arrAngle, strtof(strAngles[i])) + json_array_append_real(arrVAngle, strtof(strVAngles[i])) + } + } + + json_object_set_value(spawn, "origin", arrOrigin) + json_object_set_value(spawn, "angle", arrAngle) + json_object_set_value(spawn, "vAngle", arrVAngle) + + json_free(arrOrigin) + json_free(arrAngle) + json_free(arrVAngle) + + json_array_append_value(arrSpawns, spawn) + + json_free(spawn) + } + } + + fclose(f) + + new JSON: objSpawns = json_init_object() + json_object_set_value(objSpawns, "spawns", arrSpawns) + json_free(arrSpawns) + + new filePath[PLATFORM_MAX_PATH] + get_datadir(filePath, charsmax(filePath)) + + formatex(filePath, charsmax(filePath), "%s/redm/converted", filePath) + if (!dir_exists(filePath) && mkdir(filePath) == -1) + set_fail_state("Can't create folder `%s`", filePath) + + formatex(filePath, charsmax(filePath), "%s/%s.spawns.json", + filePath, map + ) + + if (!json_serial_to_file(objSpawns, filePath, true)) + set_fail_state("Can't create file `%s`", filePath) + + json_free(objSpawns) + return true +} + +static GameDLLSpawnsCountFix() { + set_member_game(m_bLevelInitialized, true) + set_member_game(m_iSpawnPointCount_CT, 32) + set_member_game(m_iSpawnPointCount_Terrorist, 32) +} + +public bool: SpawnPreset_DefaultPreset(const player) { + if (g_arrSpawns == Invalid_JSON) + return false + + new count = json_array_get_count(g_arrSpawns) + if (!count) + return false + + new team = get_member(player, m_iTeam) + switch(mp_randomspawn) { + case 0: + return false + case 2: { + if (team != _: TEAM_TERRORIST) + return false + } + case 3: { + if (team != _: TEAM_CT) + return false + } + } + + return Player_MoveToSpawn(player, count) +} + +static bool: Player_MoveToSpawn(const player, const count) { + new team = mp_freeforall ? 0 : get_member(player, m_iTeam) + + new bestSpawnIdx = -1 + for (new attempt; attempt <= 2 && bestSpawnIdx == -1; attempt++) { + new spawnsCount = count + + // Set random offset for starting search + new maxSpawn = count - 1 + new potentialSpawnIdx = random_num(0, maxSpawn) + while(spawnsCount--) { + // Reset iterator + if (++potentialSpawnIdx > maxSpawn) { + potentialSpawnIdx = 0 + } + + // TODO: optimize that! + if (Spawn_CheckConditions(player, team, potentialSpawnIdx, attempt)) { + bestSpawnIdx = potentialSpawnIdx + break + } else if (attempt == 2) { + break + } + } + } + + if (bestSpawnIdx == -1) { + LogMessageEx(Warning, "Player %n can't found good spawn point", + player + ) + + return false + } + + new Float: origin[3], Float: angle[3], Float: vAngle[3] + GetSpawnFromObject(bestSpawnIdx, origin, angle, vAngle) + new bool: res = Spawn_EntitySetPosition(player, origin, angle, vAngle) + if (!res) { + LogMessageEx(Debug, "Player %n can't use a spawn point %i[%.2f,%.2f,%.2f]", + player, bestSpawnIdx, + origin[0], origin[1], origin[2] + ) + } + + return res +} + +static bool: Spawn_CheckConditions(const target, const targetTeam, const spawnIdx, const attempt) { + new Float: spawnOrigin[3], Float: spawnAngle[3], Float: spawnVAngle[3], spawnTeam, spawnGroup[32] + GetSpawnFromObject(spawnIdx, spawnOrigin, spawnAngle, spawnVAngle, spawnTeam, spawnGroup) + + // Doesn't match because of the spawn command + if (targetTeam && spawnTeam && (spawnTeam != targetTeam)) + return false + + // TODO: implement spawn group check + // if (spawnGroup[0] != EOS && spawnGroup[0] == 'A') + // return false + + for (new i = 1; i <= MaxClients; i++) { + if (target == i) + continue + + if (!is_user_alive(i)) + continue + + // Exclude teammates if need it + if (targetTeam && targetTeam == pev(i, pev_team)) + continue + + new Float: enemyOrigin[3] + pev(i, pev_origin, enemyOrigin) + + new Float: disatanceToEnemy = get_distance_f(spawnOrigin, enemyOrigin) + new Float: searchDistance = mp_randomspawn_dist / (attempt + 1) + + if (disatanceToEnemy < 200.0) + return false + + if (disatanceToEnemy > searchDistance) + continue + + spawnOrigin[2] += 17.0 // check the head + if (mp_randomspawn_los) { + if (/* fm_is_in_viewcone(i, spawnOrigin) && */ fm_is_visible(i, spawnOrigin, true)) { + return false + } + } + } + + return true +} + +static GetSpawnFromObject(const spawnIdx, Float: origin[3], Float: angle[3], Float: vAngle[3], &team = 0, spawnGroup[] = "") { + new JSON: spawn = json_array_get_value(g_arrSpawns, spawnIdx) + team = json_object_get_number(spawn, "team") + new JSON: arrOrigin = json_object_get_value(spawn, "origin") + new JSON: arrAngle = json_object_get_value(spawn, "angle") + new JSON: arrVAngle = json_object_get_value(spawn, "vAngle") + json_object_get_string(spawn, "group", spawnGroup, 31) + + for (new i; i < sizeof(origin); i++) { + origin[i] = json_array_get_real(arrOrigin, i) + if (i < 2) { + angle[i] = json_array_get_real(arrAngle, i) + vAngle[i] = json_array_get_real(arrVAngle, i) + } + } + + json_free(arrOrigin) + json_free(arrAngle) + json_free(arrVAngle) + json_free(spawn) +} + +// TODO: for optimize +static stock GetPlayersOrigin(const startPlayer, Float: playersOrigin[MAX_PLAYERS + 1][3]) { + for (new i = 1; i <= MaxClients; i++) { + if (i != startPlayer && !is_user_alive(i)) + continue + + new Float: origin[3] + pev(i, pev_origin, origin) + + new Float: view_ofs[3] + pev(i, pev_view_ofs, view_ofs) + + xs_vec_add(playersOrigin[i], origin, view_ofs) + } +}