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 @@
+
+
+
+
+
+
+
+ AMXModX plugins to provide Deathmatch gameplay in Counter-Strike 1.6 optimized to work with ReGameDLL_CS.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## 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)
+ }
+}