diff --git a/.github/release.yml b/.github/release.yml index e6b97c026..461d7ec71 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -9,6 +9,9 @@ changelog: - title: Exciting New Features 🎉 labels: - enhancement + - title: Bug Fixes 🐞 + labels: + - "bug" - title: Other Changes labels: - "*" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 010cc46b7..eee7928c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -92,6 +92,7 @@ jobs: token: "${{ secrets.GITHUB_TOKEN }}" artifacts: "artifacts/${{ needs.build.outputs.filename }}.zip,artifacts/${{ needs.build.outputs.filename }}.tar.gz" prerelease: true + draft: true commit: "${{ github.sha }}" tag: "v${{ needs.build.outputs.get5-version }}-${{ needs.build.outputs.sha-short }}" name: "Nightly ${{ needs.build.outputs.get5-version }}-${{ needs.build.outputs.sha-short }}" diff --git a/.github/workflows/generate_docs.yml b/.github/workflows/generate_docs.yml index 7bc5c5ecb..09ac6837a 100644 --- a/.github/workflows/generate_docs.yml +++ b/.github/workflows/generate_docs.yml @@ -4,6 +4,7 @@ on: branches: - "master" - "development" + - "adjust_docs" jobs: build: name: Deploy docs diff --git a/cfg/get5/live.cfg b/cfg/get5/live.cfg index f63f70b17..cd1ee6e91 100644 --- a/cfg/get5/live.cfg +++ b/cfg/get5/live.cfg @@ -55,6 +55,4 @@ sv_holiday_mode 0 sv_talk_enemy_dead 0 sv_talk_enemy_living 0 sv_voiceenable 1 -tv_delay 105 -tv_delaymapchange 1 tv_relayvoice 0 diff --git a/cfg/get5/warmup.cfg b/cfg/get5/warmup.cfg index 32e2a166f..ea518e163 100644 --- a/cfg/get5/warmup.cfg +++ b/cfg/get5/warmup.cfg @@ -15,7 +15,6 @@ mp_solid_teammates 0 mp_spectators_max 20 mp_startmoney 16000 mp_timelimit 0 -mp_warmuptime_all_players_connected 0 sv_alltalk 1 sv_auto_full_alltalk_during_warmup_half_end 1 sv_coaching_enabled 1 @@ -27,8 +26,5 @@ sv_hibernate_when_empty 0 sv_infinite_ammo 0 sv_showimpacts 0 sv_voiceenable 1 -tv_delay 105 -tv_delaymapchange 1 tv_relayvoice 0 - sv_cheats 0 diff --git a/configs/get5/example_match.cfg b/configs/get5/example_match.cfg index 794d71156..99fb40630 100644 --- a/configs/get5/example_match.cfg +++ b/configs/get5/example_match.cfg @@ -81,5 +81,6 @@ "cvars" { "hostname" "Match server #1" + "sm_practicemode_can_be_started" "0" // Disallow enabling practice mode when a match is loaded. } } diff --git a/configs/get5/example_match.json b/configs/get5/example_match.json index 2c7e779d3..7482e7035 100644 --- a/configs/get5/example_match.json +++ b/configs/get5/example_match.json @@ -63,6 +63,7 @@ }, "cvars": { - "hostname": "Match server #1" + "hostname": "Match server #1", + "sm_practicemode_can_be_started": "0" } } diff --git a/configs/get5/scrim_template.cfg b/configs/get5/scrim_template.cfg index 288a737d9..a39edc237 100644 --- a/configs/get5/scrim_template.cfg +++ b/configs/get5/scrim_template.cfg @@ -36,5 +36,6 @@ "get5_demo_name_format" "scrim_{TIME}_{MAPNAME}" // Set to "" to disable recording "get5_kick_when_no_match_loaded" "0" "get5_print_damage" "1" // Enabling will print damage on round-end. + "sm_practicemode_can_be_started" "0" // Disallow enabling practice mode when a match is loaded. } } diff --git a/documentation/docs/backup.md b/documentation/docs/backup.md index 2e828b2a1..5aa096849 100644 --- a/documentation/docs/backup.md +++ b/documentation/docs/backup.md @@ -10,19 +10,20 @@ the entire [match configuration](../match_schema) and the match series score for The backup system must be [enabled](../configuration/#get5_backup_system_enabled) for this to work. -## How does it work? +### How does it work? Every time a round starts, CS:GO automatically writes a round backup file into the root of the `csgo` directory based on the value of `mp_backup_round_file`. The default value for this is `backup`. Get5 reads this file and copies it into its -own file called `get5_backup_match%s_map%d_round%d.cfg`, where the arguments are `matchid`, `mapnumber` and `roundnumber`, -respectively. A special backup called `get5_backup_match%s_map%d_prelive.cfg` is created for the knife round. +own file called `get5_backup_match%s_map%d_round%d.cfg`, where the arguments are `matchid`, `mapnumber` +and `roundnumber`, respectively. A special backup called `get5_backup_match%s_map%d_prelive.cfg` is created and should +be used if you want to restore to the beginning of the map, before the knife round. -## Example +### Example When in a match, you can call [`get5_listbackups`](../commands/#get5_listbackups) to view all backups for the current match. Note that all rounds and map numbers start at 0. -They print in the format `filename date team1 team2 map team1_score team2_score`. +They print in the format `filepath date time team1 team2 map team1_score team2_score`. ``` > get5_listbackups @@ -37,8 +38,16 @@ get5_backup_match1844_map0_round17.cfg 2022-07-26 19:03:39 "Team A" "Team B" de_ ``` To load at the beginning of round 13 of the first map of match ID 1844, all players should be connected to the server, -and you can type: +and you use the [`get5_loadbackup`](../commands/#get5_loadbackup) command: -`get5_loadbackup get5_backup_match1844_map0_round12.cfg`. +`get5_loadbackup get5_backup_match1844_map0_round12.cfg`. The game should restore in a paused state and both teams must [`!unpause`](../commands/#unpause) to continue. + +### Pauses in backups + +When restoring from a backup, the [consumed pauses](pausing.md) are reset to the state they were in at the beginning +of the round you restore to, but only if the game state is not currently live. This means that using +the [`!stop`](../commands/#stop) command or the [`get5_loadbackup`](../commands/#get5_loadbackup) command **for the same +match and map** would retain the currently used pauses. If restarting the server or loading the backup from scratch, the +pauses from the backup file will be used. diff --git a/documentation/docs/coaching.md b/documentation/docs/coaching.md new file mode 100644 index 000000000..43bf21124 --- /dev/null +++ b/documentation/docs/coaching.md @@ -0,0 +1,61 @@ +# :material-headset: Coaching + +Get5 ships with mechanics to manage coaches, but the behavior differs _slightly_ from the built-in coaching +system found in the game, which avoids a +few ["minor" bugs](https://en.wikipedia.org/wiki/Counter-Strike_coaching_bug_scandal). + +### Server requirements {: #requirements } + +1. [`sv_coaching_enabled`](https://totalcsgo.com/command/svcoachingenabled) must be set to 1. +2. [`coaches_per_team`](../match_schema/#schema) in your match configuration + or [scrim](../getting_started/#scrims) template must be larger than 0. +3. The [`-maxplayers_override`](https://developer.valvesoftware.com/wiki/Maxplayers) + launch parameter must be defined on your server to allow for the number of connected clients you expect, including + all players, spectators and coaches. + +### Becoming a coach {: #howto } + +Due to internal conflicts with how [backups](backup.md) and auto-assignment to teams works in Get5, the default +[`coach`](https://counterstrike.fandom.com/wiki/Coaching) console command is disabled. You can become a coach in one of +three ways: + +1. Use the [`!coach`](../commands/#coach) chat command during warmup. +2. Be defined as a coach in the [match configuration](../match_schema/#schema) or + via [`get5_addcoach`](../commands/#get5_addcoach). +3. Join a game where the team is already full (determined by [`players_per_team`](../match_schema/#schema)) and where a + coach slot is available. + +!!! warning "Coaching is permanent after warmup" + + Once a game begins (goes past the warmup-phase), you cannot enter or leave the coach slot unless you are removed + from coaching using [`get5_removeplayer`](../commands/#get5_removeplayer). + +If the current number of coaches exceeds or equals [`coaches_per_team`](../match_schema/#schema), including if it is +zero, additional players will be kicked from the match. However, if a connecting player is defined +in [`players`](../match_schema/#schema), the team is full and a coach slot is open, they will be moved to coaching for +the series and can only stop coaching if the game is still in warmup. + +This behavior allows you to define as many coaches and players in the match configuration as you want: As long as the +number of players and coaches on the server don't exceed [`players_per_team`](../match_schema/#schema) +and [`coaches_per_team`](../match_schema/#schema), respectively, Get5 will fill the +game's slots with the appropriate number of players and coaches and kick the rest. Being in +the [`coaches`](../match_schema/#schema) section takes precedence over [`players`](../match_schema/#schema). + +!!! note "Decreasing the number of players" + + If a match configuration with [`players_per_team`](../match_schema/#schema) or + [`coaches_per_team`](../match_schema/#schema) set to a number *lower* than the number of players **already connected + to the server**, the entire team's players or coaches (whichever is exceeded) will be kicked and must reconnect. + +### Coaching in scrims {: #scrims } + +When in [scrim mode](../getting_started/#scrims), you cannot set the [`coaches`](../match_schema/#schema) key, and +players are never _locked_ to the coaching slot. This means that to become a coach in a scrim, you must always +call [`!coach`](../commands/#coach) or join a team that already has [`players_per_team`](../match_schema/#schema) +players (i.e. is full). + +!!! danger "`players_per_team` matters!" + + Do not set [`players_per_team`](../match_schema/#schema) in your scrim template to a value larger than the number of + players you expect. If you do this, a coach - or any player defined in your scrim template - (re)connecting after + warmup will be put on the team and won't be able to become a coach. diff --git a/documentation/docs/commands.md b/documentation/docs/commands.md index 0b4092fdc..13ab69240 100644 --- a/documentation/docs/commands.md +++ b/documentation/docs/commands.md @@ -30,8 +30,8 @@ Please note that these can be typed by *all players* in chat. ####`!coach` -: Moves a client to coach for their team. Requires that -the [`sv_coaching_enabled`](https://totalcsgo.com/command/svcoachingenabled) variable is set to `1`. +: Requests to become a [coach](coaching.md) for your team. If already coaching, this will move you back as a player +if possible. Can only be used during warmup. ####`!stay` @@ -51,13 +51,13 @@ the [get5_stop_command_enabled](../configuration/#get5_stop_command_enabled) is : Force-readies your team, marking all players on your team as ready. -####`!ringer` +####`!ringer ` {: #ringer } -: Adds/removes a ringer to/from the home scrim team. +: Alias for [`get5_ringer`](#get5_ringer). ####`!scrim` -: Shortcut for [`get5_scrim`](#get5_scrim). +: Alias for [`get5_scrim`](#get5_scrim). ####`!get5` @@ -74,8 +74,9 @@ Please note that these are meant to be used by *admins* in console. : Loads a [match configuration](../match_schema) file (JSON or KeyValue) relative from the `csgo` directory. ####`get5_loadbackup ` {: #get5_loadbackup } -: Loads a match backup file (JSON or KeyValue) relative from the `csgo` -directory. Only works if the [backup system is enabled](../configuration/#get5_backup_system_enabled). +: Loads a match backup, relative from the `csgo` +directory. Only works if the [backup system is enabled](../configuration/#get5_backup_system_enabled). If you define +[`get5_backup_path`](../configuration/#get5_backup_path), you must include the path in the filename. ####`get5_last_backup_file` : Prints the name of the last match backup file Get5 wrote in the current series, this is automatically updated each @@ -93,16 +94,18 @@ You should put the `url` argument inside quotation marks (`""`). Loading remote matches requires the [SteamWorks](../installation/#steamworks) extension. -####`get5_endmatch` -: Force ends the current match. No winner is set (draw). +####`get5_endmatch [team1|team2]` {: #get5_endmatch } +: Force-ends the current match. The team argument will force the winner of the series and the current map to be set +to that team. Omitting the team argument sets no winner (tie). -####`get5_creatematch` -: Creates a BO1 match with the current players on the server on the current map. +####`get5_creatematch [map name] [matchid]` {: #get5_creatematch } +: Creates a BO1 match with the current players on the server. `map name` defaults to the current map and `matchid` +defaults to `manual`. You should **not** provide a match ID if you use the [MySQL extension](../stats_system/#mysql). ####`get5_scrim [opposing team name] [map name] [matchid]` {: #get5_scrim } -: Creates a [scrim](../getting_started/#scrims) on the current map. For example, if you're - playing *fnatic* on `de_dust2` you might run `get5_scrim fnatic de_dust2`. The other team name defaults to "away" - and the map defaults to the current map. `matchid` defaults to an empty string. +: Creates a [scrim](../getting_started/#scrims) on the current map. The opposing team name defaults to `Away` +and the map defaults to the current map. `matchid` defaults to `scrim`. You should **not** provide a match ID if +you use the [MySQL extension](../stats_system/#mysql). ####`get5_addplayer [name]` {: #get5_addplayer } : Adds a Steam ID to a team (can be any format for the Steam ID). The name parameter optionally locks the player's @@ -112,8 +115,10 @@ name. : Adds a Steam ID to a team as a coach. The name parameter optionally locks the player's name. -####`get5_removeplayer ` -: Removes a steam ID from all teams (can be any format for the Steam ID). +####`get5_removeplayer ` {: #get5_removeplayer} +: Removes a steam ID from all teams (can be any format for the Steam ID). This also removes the player as +a [coach](coaching.md). If [`get5_check_auths`](../configuration/#get5_check_auths) is set, the player will be removed +from the server immediately. ####`get5_addkickedplayer [name]` {: #get5_addkickedplayer } : Adds the last kicked Steam ID to a team. The name parameter optionally locks the player's name. @@ -124,9 +129,6 @@ name. ####`get5_forceready` : Marks all teams as ready. `get5_forcestart` does the same thing. -####`get5_dumpstats` -: Dumps current match stats to a file. - ####`get5_status` : Replies with JSON formatted match state (available to all clients). @@ -220,15 +222,28 @@ name. ``` ####`get5_listbackups [matchid]` {: #get5_listbackups } -: Lists backup files for the current match or a given match ID if provided. +: Lists backup files for the current match or a given match ID if provided. If you define +[`get5_backup_path`](../configuration/#get5_backup_path), it will only list backups found under that prefix. + +####`get5_ringer ` {: #get5_ringer } +: Adds/removes a ringer to/from the home scrim team. `target` is the name of the player, their user ID or their Steam +ID. Similar to [`!ringer`](../commands/#ringer) in chat. + +!!! example "User ID vs client index" -####`get5_ringer ` -: Adds/removes a ringer to/from the home scrim team. `player` is the name of the player. Similar -to [`!ringer`](../commands/#ringer) + To view user IDs, type `users` in console. In this example, `3` is the user ID and `1` is the client index: + ``` + > users + 1:3:"Quinn" + ``` ####`get5_debuginfo [file]` {: #get5_debuginfo } : Dumps debug info to a file (`addons/sourcemod/logs/get5_debuginfo.txt` if no file parameter is provided). +####`get5_dumpstats [file]` {: #get5_dumpstats } +: Dumps [player stats](../stats_system/#keyvalue) to a file (`addons/sourcemod/get5_matchstats.cfg` if no file +parameter is provided). + ####`get5_test` : Runs get5 tests. **This should not be used on a live match server since it will reload a match config to test**. diff --git a/documentation/docs/configuration.md b/documentation/docs/configuration.md index 3affd2889..0e6bb164c 100644 --- a/documentation/docs/configuration.md +++ b/documentation/docs/configuration.md @@ -17,11 +17,9 @@ the explanation of the [match schema](../match_schema), that section will overri ### Phase Configuration Files You should also have three config files. These can be edited, but we recommend not -blindly pasting another config in (e.g. ESL, CEVO). Configs that execute warmup commands (`mp_warmup_end`, for -example) **will** cause problems. These must only include commands you would run in the console (such +blindly pasting another config in (e.g. ESL, CEVO). These must only include commands you would run in the console (such as `mp_friendly_fire 1`) and should determine the rules for those three stage of your match. You can -also [point to other files](#config-files) by editing -the main config file. +also [point to other files](#config-files) by editing the main config file. ```yaml cfg/get5/warmup.cfg # (1) @@ -33,6 +31,27 @@ cfg/get5/live.cfg # (3) 2. Executed when the knife-round starts. 3. Executed when the game goes live. +!!! danger "Prohibited options" + + You should avoid these commands in your live, knife and warmup configuration files, as all of these are handled by + Get5 automatically. Introducing restarts, warmup changes or [GOTV](gotv.md) delay modifications can cause problems. + If you want to set your `tv_delay`, do it in the `cvars` section of your [match configuration](match_schema.md). + + ``` + mp_do_warmup_period + mp_restartgame + mp_warmup_end + mp_warmup_pausetimer + mp_warmup_start + mp_warmuptime + mp_warmuptime_all_players_connected + tv_delay + tv_delaymapchange + tv_enable + tv_record + tv_stoprecord + ``` + ## Server Setup **These options will generally not be directly presented to clients.** @@ -49,7 +68,8 @@ cfg/get5/live.cfg # (3) : Whether the [`!stop`](../commands/#stop) command is enabled. **`Default: 1`** ####`get5_kick_when_no_match_loaded` -: Whether to kick all clients if no match is loaded. **`Default: 0`** +: Whether to kick all clients if no match is loaded. Players will not be kicked if a match is forcefully ended +using [`get5_endmatch`](../commands/#get5_endmatch). **`Default: 0`** ####`get5_end_match_on_empty_server` : Whether the match is ended with no winner if all players leave (note: this will happen even if all players @@ -59,12 +79,13 @@ disconnect even in warmup with the intention to reconnect!). **`Default: 0`** : Whether to wait for map vetoes to be printed to GOTV before changing map. **`Default: 0`** ####`get5_check_auths` -: Whether the Steam IDs from a "players" section are used to force players onto teams, and will kick -users if they are not in the auth list. **`Default: 1`** +: Whether the Steam IDs from the `players` and `coaches` sections of a [match configuration](../match_schema/#schema) +are used to force players onto teams. Anyone not defined will be removed from the game, or if +in [scrim mode](../getting_started/#scrims), put on `team2`. **`Default: 1`** ####`get5_print_update_notice` : Whether to print to chat when the game goes live if a new version of Get5 is available. This only works if - [SteamWorks](../installation/#steamworks) has been installed. **`Default: 1`** +[SteamWorks](../installation/#steamworks) has been installed. **`Default: 1`** ####`get5_pretty_print_json` : Whether to pretty-print all JSON output. This also affects the output of JSON in the @@ -134,9 +155,25 @@ if [`get5_print_damage`](#get5_print_damage) is disabled. - [-] (30 in 1) to [-] (0 in 0) from Player5 (0 HP) # - dealt damage to this player, not enough for assist ``` +####`get5_phase_announcement_count` +: The number of times the "Knife" or "Match is LIVE" announcements will be printed in chat. Set to zero to disable. +**`Default: 5`** + ####`get5_message_prefix` -: The tag applied before plugin messages. If you change this variable, `Powered by Get5` will be printed when the game -goes live. **`Default: Get5`** +: The tag applied before plugin messages. Note that at least one character must come before +a [color modifier](#color-substitutes). **`Default: "[{YELLOW}Get5{NORMAL}]"`** + +####`get5_team1_color` +: The [color](#color-substitutes) to use when printing the name of `team1` in chat +messages.
**`Default: "{LIGHT_GREEN}"`** + +####`get5_team2_color` +: The [color](#color-substitutes) to use when printing the name of `team2` in chat +messages.
**`Default: "{PINK}"`** + +####`get5_spec_color` +: The [color](#color-substitutes) to use when printing the name of `spectators` in chat +messages.
**`Default: "{NORMAL}"`** ## Pausing @@ -154,9 +191,8 @@ if [get5_fixed_pause_time](#get5_fixed_pause_time) is set to a non-zero value. **`Default: 300 (5 minutes)`** ####`get5_fixed_pause_time` -: If non-zero, the fixed length in seconds all [`tactical`](../pausing/#tactical) pauses will be. Adjusting this to -non-zero will use the in-game timeout counter, and the [get5_max_pause_time](#get5_max_pause_time) -parameter is ignored. **`Default: 0`** +: If non-zero, the fixed length in seconds of all [`tactical`](../pausing/#tactical) pauses. This takes precedence +over the [get5_max_pause_time](#get5_max_pause_time) parameter, which will be ignored. **`Default: 0`** ####`get5_allow_technical_pause` : Whether [technical pauses](../pausing/#technical) are available to clients or not. **`Default: 1`** @@ -182,17 +218,20 @@ must confirm. **`Default: 0`** ####`get5_time_format` : Time format string. This determines the [`{TIME}`](#tag-time) tag. **Do not change this unless you know what you are -doing! Avoid using spaces or colons.** **`Default: %Y-%m-%d_%H`** +doing! Avoid using spaces or colons.** **`Default: "%Y-%m-%d_%H-%M-%S"`** ####`get5_demo_name_format` -: Format to name demo files. Set to empty string to disable. **`Default: {MATCHID}_map{MAPNUMBER}_{MAPNAME}`** +: Format to use for demo files when [recording matches](gotv.md). Do not include a file extension (`.dem` is added +automatically). Set to empty string to disable. If you do not include the [`{TIME}`](#tag-time) tag, you will have +problems with duplicate files if restoring a game from a backup.
Note that the [`{MAPNUMBER}`](#tag-mapnumber) +variable is not zero-indexed!
**`Default: "{TIME}_{MATCHID}_map{MAPNUMBER}_{MAPNAME}"`** ####`get5_event_log_format` : Format to write event logs to. Set to empty string to disable. **`Default: ""`** ####`get5_stats_path_format` : Path where stats are output at each map end if it is set. Set to empty string to -disable. **`Default: get5_matchstats_{MATCHID}.cfg`** +disable. **`Default: "get5_matchstats_{MATCHID}.cfg"`** ## Backup System @@ -201,20 +240,42 @@ disable. **`Default: get5_matchstats_{MATCHID}.cfg`** command as well as the [`get5_loadbackup`](../commands/#get5_loadbackup) command. **`Default: 1`** ####`get5_max_backup_age` -: Number of seconds before a Get5 backup file is automatically deleted. 0 to disable. **`Default: 160000`** +: Number of seconds before a Get5 backup file is automatically deleted. 0 to disable. If you define +[`get5_backup_path`](#get5_backup_path), only files in that path will be deleted. **`Default: 160000`** + +####`get5_backup_path` +: The folder of saved [backup files](../commands/#get5_loadbackup), relative to the `csgo` directory. You **can** use +the [`{MATCHID}`](#tag-matchid) variable, i.e. `backups/{MATCHID}/`. **`Default: ""`** + +!!! warning "Slash, slash, hundred yard dash :material-slash-forward:" + + It is very important that your backup path does **not** start with a slash but instead **ends with a slash**. If + not, the last part of the path will be considered a prefix of the filename and things will not work correctly. Also + note that if you use the [`{MATCHID}`](#tag-matchid) variable, [automatic deletion of backups](#get5_max_backup_age) + does not work. + + :white_check_mark: `backups/` + + :white_check_mark: `backups/{MATCHID}/` + + :no_entry: `/backups/` + + :no_entry: `/backups/{MATCHID}` ## Config Files ####`get5_live_cfg` -: Config file executed when the game goes live. **`Default: get5/live.cfg`** - -####`get5_autoload_config` -: A config file to autoload on map starts if no match is loaded, relative to the `csgo` directory. Set to empty -string -to disable. **`Default: ""`** +: Config file executed when the game goes live, relative to `csgo/cfg`.
**`Default: "get5/live.cfg"`** ####`get5_warmup_cfg` -: Config file executed in warmup periods. **`Default: get5/warmup.cfg`** +: Config file executed in warmup periods, relative to `csgo/cfg`.
**`Default: "get5/warmup.cfg"`** + +####`get5_knife_cfg` +: Config file executed for the knife round, relative to `csgo/cfg`.
**`Default: "get5/knife.cfg"`** + +####`get5_autoload_config` +: A [match configuration](../match_schema/#schema) file, relative to the `csgo` directory, to autoload when a player +joins the server if no match is loaded. Set to empty string to disable. **`Default: ""`** ## Substitution Variables @@ -253,7 +314,7 @@ placeholder strings that will be replaced by meaningful values when printed. ### Colour Substitutes {: #color-substitutes } These variables can be used to color text in the chat. You must return to `{NORMAL}` (white) -after using a color variable. Note that a color prefix cannot be _followed by a space_. +after using a color variable. Example: `This text becomes {DARK_RED}red{NORMAL}, while {YELLOW}all of this will be yellow`. @@ -269,3 +330,4 @@ Example: `This text becomes {DARK_RED}red{NORMAL}, while {YELLOW}all of this wil - `{LIGHT_BLUE}` - `{DARK_BLUE}` - `{PURPLE}` +- `{GOLD}` diff --git a/documentation/docs/developer_api.md b/documentation/docs/developer_api.md index f194cf942..6b009be6f 100644 --- a/documentation/docs/developer_api.md +++ b/documentation/docs/developer_api.md @@ -1,4 +1,4 @@ -# Developer API +# :material-code-braces: Developer API Get5 can be interacted with in several ways. At a glance: diff --git a/documentation/docs/event_schema.yml b/documentation/docs/event_schema.yml index ab03123e0..c6ec1149e 100644 --- a/documentation/docs/event_schema.yml +++ b/documentation/docs/event_schema.yml @@ -489,14 +489,15 @@ paths: tags: - Series Flow description: | - Fired when a round is restored from a backup. + Fired when a round is restored from a backup. Note that the map and round numbers indicate the round being + restored **to**, not the round the backup was requested during. requestBody: content: application/json: schema: title: Get5BackupRestoredEvent allOf: - - "$ref": "#/components/schemas/Get5MapEvent" + - "$ref": "#/components/schemas/Get5RoundEvent" properties: event: enum: diff --git a/documentation/docs/getting_started.md b/documentation/docs/getting_started.md index b56e16d5f..185b387a8 100644 --- a/documentation/docs/getting_started.md +++ b/documentation/docs/getting_started.md @@ -5,7 +5,7 @@ While you can just jump right in, we recommend you read the [configuration](../configuration) and [match schema](../match_schema) sections of the documentation to understand what Get5 can do. -## Quick Start +## Quick Start {: #quick-start } If you want to create a match quickly without modifying anything, you must set two properties: @@ -18,17 +18,25 @@ call [`get5_creatematch`](../commands/#get5_creatematch). There is also a simple from by typing [`!get5`](../commands/#get5) in the game chat. Note that you must be [a server administrator](../installation/#administrators) to do this. -## Scrims +## Match Configuration {: #match-configuration } -While Get5 is intended for matches (league matches, LAN-matches, cups, etc.), it can be used for everyday -scrims/gathers/whatever as well. If that is your use case, you should do a few things differently. We call "_having a -home team defined and anyone else on the opposing team_" a **scrim**. +The default operation mode for Get5 is the configuration and loading of +a [match configuration file](../match_schema). This file should contain all the players and coaches, their team +name and optionally flag and logo as well as any spectators/casters. Once you've created your file you can load it +using the [`get5_loadmatch`](../commands/#get5_loadmatch) command or configure your server to automatically load the +file as soon as a player joins by setting [`get5_autoload_config`](../configuration/#get5_autoload_config). -### Letting the opposing team in {: #opposing-team } +!!! tip "Lock it down" -Get5 can be configured to kick all players from the server if no match is loaded. You should disable this for a scrim -server. To do so, edit [`cfg/sourcemod/get5.cfg`](../configuration/#main-config) and make sure that -[`get5_kick_when_no_match_loaded`](../configuration/#get5_kick_when_no_match_loaded) to `0`. + When loading match configurations, ensure that [`get5_check_auths`](../configuration/#get5_check_auths) is enabled. + This ensures that people are locked to the correct teams and that nobody else can join the server. + +## Scrims {: #scrims } + +While Get5 is intended for matches (league matches, LANs, cups, etc.), it can be used for everyday +scrims or gathers as well. If that is your use case, you should do a few things differently. We call "_having a +home team defined and anyone else on the opposing team_" a **scrim**, and loading this configuration is referred to as +**scrim mode**. ### Adding your team's Steam IDs {: #home-team } @@ -37,10 +45,24 @@ located at `addons/sourcemod/configs/get5/scrim_template.cfg` and add in *your* their Steam IDs (any format works). After doing this, any user who does not belong in `team1` will implicitly be set to `team2`. +!!! warning "Coaches in scrims" + + You **cannot** set the [`coaches`](../match_schema/#schema) section in a scrim template. Instead, add everyone to + the [`players`](../match_schema/#schema) section and use the [`!coach`](../commands/#coach) command to become a + [coach](coaching.md) after joining the game. If the team is full (defined by + [`players_per_team`](../match_schema/#schema)), additional players will automatically be moved to coach if there are + available slots. + You can list however many players you want. Add all your coaches, analysts, ringers, and such. If someone on your list ends up being on the other team in a scrim, you can use the [`!ringer`](../commands/#ringer) command to temporarily swap them (similarly, you can use it to put someone not in the list on your team temporarily). +### Letting the opposing team in {: #opposing-team } + +Get5 can be configured to kick all players from the server if no match is loaded. You should disable this for a scrim +server. To do so, edit [`cfg/sourcemod/get5.cfg`](../configuration/#main-config) and make sure that +[`get5_kick_when_no_match_loaded`](../configuration/#get5_kick_when_no_match_loaded) is set to `0`. + ### Starting the Match Rather than creating a [match configuration](match_schema.md), you should @@ -48,12 +70,14 @@ use the [`get5_scrim`](../commands/#get5_scrim) command when the server is on th RCON or as a regular console command if you are [a server administrator](../installation/#administrators). You could also type [`!scrim`](../commands/#scrim) in chat. -Once you've done this, all that has to happen is teams to [ready up](../commands/#ready) to start the match. +Once you've done this, all that is required is for both teams to [ready up](../commands/#ready) and the match will +begin. !!! danger "Practice Mode" If you have [practicemode](https://github.com/splewis/csgo-practice-mode) on your server as well, you may wish to - add `sm_practicemode_can_be_started 0` in your [live configuration](../configuration/#phase-configuration-files). + add `sm_practicemode_can_be_started 0` in the `cvars` section of your [match configuration](../match_schema/#schema). + This will remove the ability to start practice mode until the match is completed or cancelled. ### Changing Scrim Settings @@ -61,4 +85,5 @@ You can (and should) edit the [scrim template](https://github.com/splewis/get5/blob/master/configs/get5/scrim_template.cfg) at `addons/sourcemod/configs/get5/scrim_template.cfg`. In this you can set any scrim-specific properties in the `cvars` section. The template defaults to `mp_match_can_clinch 0` (designed for practice) which you should disable if playing a -real match. You may also want to lower `tv_delay` (and maybe `tv_enable` so you can record your scrims). +real match. You may also want to lower `tv_delay` (and maybe set `tv_enable 1` so you can [record your scrims](gotv.md)) +. diff --git a/documentation/docs/gotv.md b/documentation/docs/gotv.md new file mode 100644 index 000000000..550c9600b --- /dev/null +++ b/documentation/docs/gotv.md @@ -0,0 +1,27 @@ +# :material-filmstrip: GOTV & Demos {: #gotv } + +Get5 can be configured to automatically record matches. This is enabled by default based on the state +of [`get5_demo_name_format`](../configuration/#get5_demo_name_format) and can be disabled by setting that parameter to +an empty string. + +Demo recording starts once all teams have readied up and ends shortly following a map result. When a demo file is +written to disk, the [`Get5_OnDemoFinished`](events_and_forwards.md) forward is called, which you can use to move the +file or upload it somewhere. The filename can also be found in the map-section of the +[KeyValue stats system](../stats_system/#keyvalue). + +Get5 will automatically adjust the [`mp_match_restart_delay`](https://totalcsgo.com/command/mpmatchrestartdelay) when a +map ends if GOTV is enabled to ensure that it won't be shorter than what is required for the GOTV broadcast to finish. +Players will also not be [kicked from the server](../configuration/#get5_kick_when_no_match_loaded) before this delay +has passed. + +!!! warning "Don't mess too much with the TV! :tv:" + + Changing `tv_delay` or `tv_enable` in `warmup.cfg`, `live.cfg` etc. is going to cause problems with your demos. + We recommend you set `tv_delay` either on your server in general or only once in the `cvars` section of your + [match configuration](../match_schema). You should also not set `tv_delaymapchange` as Get5 handles this + automatically. + + We recommend that you **do not** set `tv_enable` in your match configuration, as it **requires** a map change for + the GOTV bot to join the server. You should enable GOTV in your general server config and refrain from turning it on + and off with Get5. Note that setting `tv_enable 1` won't allow people to join your server's GOTV. You must also set + `tv_advertise_watchable 1`, so you don't have to worry about ghosting if this is disabled. diff --git a/documentation/docs/index.md b/documentation/docs/index.md index 47cde4efd..49c8848cc 100644 --- a/documentation/docs/index.md +++ b/documentation/docs/index.md @@ -11,18 +11,18 @@ functionality is built to work within how the CS:GO server normally operates, no Highlights of Get5 include: -- Locking players to their correct team and side by their Steam ID +- [Locking players to their correct team and side by their Steam ID](match_schema.md) - Automatically setting team names/logos/match text values for spectator/GOTV clients - In-game map-veto support from the match's list of maps - Support for multi-map series (Bo1, Bo2, Bo3, Bo5, etc.) - Warmup and [`!ready`](commands/#ready)-system for each team -- Automatic GOTV demo recording -- Advanced backup system built on top of Valve's backup system +- [Automatic GOTV demo recording](gotv.md) +- [Advanced backup system](backup.md) built on top of Valve's backup system - Knifing for sides -- Pausing support -- Coaching support -- Lightweight usage for scrims -- Event logging and SourceMod forwards you can interface with, allowing for collection of stats etc. +- [Advanced pausing](pausing.md) support +- [Coaching](coaching.md) support +- Lightweight usage for [scrims](getting_started/#scrims) +- [Event logging and SourceMod forwards](events_and_forwards.md) you can interface with, allowing for collection of stats etc. - [Commands](commands/#serveradmin-commands) allow remote management of the plugin If you are installing this on your game server, head over to the [Installation](./installation.md) instructions. diff --git a/documentation/docs/installation.md b/documentation/docs/installation.md index a2b47d53a..30c37b5de 100644 --- a/documentation/docs/installation.md +++ b/documentation/docs/installation.md @@ -8,23 +8,31 @@ You can get the latest versions here: [:material-download: Download MetaMod](https://www.sourcemm.net/downloads.php?branch=stable){ .md-button .md-button--primary } [:material-download: Download SourceMod](https://www.sourcemod.net/downloads.php?branch=stable){ .md-button .md-button--primary } -!!! tip +!!! info "OS is important" Remember to select the correct OS type (Windows/Linux/Mac) for **both** plugins. This should be the OS of the server. -## Download Get5 +## Get5 -The latest release of Get5 can be found [here](https://github.com/splewis/get5/releases/latest). Older Releases of -Get5 can be found in the [Releases](https://github.com/splewis/get5/releases) section of the repo. Anything *not* -marked as "Nightly" in the title are known to be stable, but may be lacking features that are currently in development. -If you would like to test new features, or be on the "bleeding edge", you can also download any of the latest -pre-releases found at the same link above that are marked in the title with "Nightly" or are marked as "Pre-release". +### Latest version {: #latest } + +The latest version of Get5 can be found here. Older releases can be found in +the [Releases](https://github.com/splewis/get5/releases) section of the repository on GitHub. + +[:material-download: Download Get5](https://github.com/splewis/get5/releases/latest){ .md-button .md-button--primary } + +### Test and development {: #development } + +If you would like to test new features, or be on the "bleeding edge", you can also download any of the releases found at +the link above that are marked in the title with **Nightly** or as **Pre-release**. Please note that these versions are +meant for testers and developers and should not be deployed to production servers unless you have a good reason to do +so or can live with the potential consequences. !!! info Get5 itself is OS-agnostic, meaning the same file works on any OS. -## Download SteamWorks (Recommended) {: #steamworks } +## SteamWorks (Recommended) {: #steamworks } SteamWorks is not required for Get5 to work on your game server, however it is required if you wish to [load match configs remotely](../commands#get5_loadmatch_url) or if you want Get5 to [automatically @@ -160,7 +168,7 @@ is just to indicate what the correct structure looks like. 1. SourceMod error logs can be found in here. This directory is empty by default. 2. This is the core Get5 plugin. 3. This is the MySQL extension for collecting stats. If you want to use this extension, please see - the [guide](../stats_system/#mysql-statistics). + the [guide](../stats_system/#mysql). 4. This is proof-of-concept integration called [get5 web panel](https://github.com/splewis/get5-web) that can be used to manage matches. **This is not supported and is probably very buggy. You should not use it.** 5. This folder contains all the language files and translations for all the plugins. @@ -170,10 +178,10 @@ is just to indicate what the correct structure looks like. 8. Don't change anything in here. There are no editable files in the `metamod` folder. It's here because SourceMod depends on it. 9. SourceMod binaries. - 10. This a JSON-example of a [match configuration]. You should use this as a template for your own match configuration. - All JSON match configurations **must** end with `.json`. - 11. The server's default scrim match configuration. This is loaded when using - the [`get5_scrim`](../commands/#get5_scrim) command. + 10. This a JSON-example of a [match configuration](match_schema.md). You should use this as a template for your own + match configuration. All JSON match configurations **must** end with `.json`. + 11. The server's default scrim [match configuration](match_schema.md). This is loaded when using the + [`get5_scrim`](../commands/#get5_scrim) command. 12. Match configurations can be created in both JSON and SourceMod's [KeyValue](https://wiki.alliedmods.net/KeyValues_(SourceMod_Scripting)) format. We recommend JSON for all new users, but Get5 will continue to support reading `.cfg` files as well. diff --git a/documentation/docs/match_schema.md b/documentation/docs/match_schema.md index 3b404eb9c..8e020f2d8 100644 --- a/documentation/docs/match_schema.md +++ b/documentation/docs/match_schema.md @@ -13,20 +13,20 @@ required to start a match. Reasonable defaults are used for the other values (Bo 5v5, empty strings for team names, etc.). We recommend using the JSON format whenever possible, as JSON has way better support in various programming languages than Valve's KeyValue format (which essentially has none). -## The schema +## The schema {: #schema } ```typescript title="TypeScript interface definition of a match configuration" -type Get5PlayerSteamID = string; // (8) -type Get5PlayerSet = { [key: Get5PlayerSteamID]: string }; // (9) +type SteamID = string // (8) +type Get5PlayerSet = { [key: SteamID]: string } | [SteamID] // (9) interface Get5MatchTeam { - "players": Get5PlayerSet, // (24) + "players": Get5PlayerSet // (24) "coaches": Get5PlayerSet // (23) - "name": string, // (16) - "tag": string, // (17) - "flag": string, // (18) - "logo": string, // (19) - "series_score": number, // (26) + "name": string // (16) + "tag": string // (17) + "flag": string // (18) + "logo": string // (19) + "series_score": number // (26) "matchtext": string // (27) } @@ -35,159 +35,349 @@ interface Get5MatchTeamFromFile { } interface Get5Match { - "match_title": string // (28) - "matchid": string, // (1) - "num_maps": number, // (2) - "players_per_team": number, // (3) - "coaches_per_team": number, // (4) - "min_players_to_ready": number, // (5) - "min_spectators_to_ready": number, // (6) - "skip_veto": boolean, // (7), - "veto_first": "team1" | "team2", // (11) - "side_type": "standard" | "always_knife" | "never_knife", // (12) + "match_title": string // (25) + "matchid": string // (1) + "clinch_series": boolean // (32) + "num_maps": number // (2) + "players_per_team": number // (3) + "coaches_per_team": number // (4) + "min_players_to_ready": number // (5) + "min_spectators_to_ready": number // (6) + "skip_veto": boolean // (7), + "veto_first": "team1" | "team2" | "random" // (11) + "side_type": "standard" | "always_knife" | "never_knife" // (12) + "map_sides": ["team1_ct" | "team1_t" | "knife"] // (31) "spectators": { // (10) "name": string // (29) "players": Get5PlayerSet // (30) }, - "map_list": [string], // (13) - "favored_percentage_team1": number, // (14) - "favored_percentage_text": string, // (15) - "team1": Get5MatchTeam | Get5MatchTeamFromFile, // (20) - "team2": Get5MatchTeam | Get5MatchTeamFromFile, // (21) + "maplist": [string] // (13) + "favored_percentage_team1": number // (14) + "favored_percentage_text": string // (15) + "team1": Get5MatchTeam | Get5MatchTeamFromFile // (20) + "team2": Get5MatchTeam | Get5MatchTeamFromFile // (21) "cvars": { [key: string]: string } // (22) } ``` -1. _Optional_ - The ID of the match. This determines the `matchid` parameter in all the forwards and events. If you use -the [MySQL extension](../stats_system/#mysql), you should leave this field blank (or omit it), as match IDs will be -assigned automatically. If you do want to assign match IDs from another source, they **must** be integers (in a string) -and must increment between matches. **`Default: ""`** -2. _Optional_ - The number of maps to play in the series. **`Default: 3`** -3. _Optional_ - The number of players per team. **`Default: 5`** -4. _Optional_ - The maximum number of coaches per team. **`Default: 2`** -5. _Optional_ - The minimum number of players of each team that must type [`!ready`](../commands/#ready) for the game to - begin. **`Default: 1`** -6. _Optional_ - The minimum number of spectators that must be [`!ready`](../commands/#ready) for the game to begin. - **`Default: 0`** -7. _Optional_ - Whether to skip the veto phase. If set to `true`, `team1` will start on CT. If `false`, sides are - determined by `side_type`. **`Default: false`** +1. _Optional_
The ID of the match. This determines the `matchid` parameter in all + [forwards and events](events_and_forwards.md). If you use the [MySQL extension](../stats_system/#mysql), you + should leave this field blank (or omit it), as match IDs will be assigned automatically. If you do want to assign + match IDs from another source, they **must** be integers (in a string) and must increment between + matches.

**`Default: ""`** +2. _Optional_
The number of maps to play in the series.

**`Default: 3`** +3. _Optional_
The number of players per team. You should **never** set this to a value higher than the number of + players you want to actually play in a game, *excluding* coaches.

**`Default: 5`** +4. _Optional_
The maximum number of [coaches](coaching.md) per team.

**`Default: 2`** +5. _Optional_
The minimum number of players that must be present for the [`!forceready`](../commands/#forceready) + command to succeed. If not forcing a team ready, **all** players must [`!ready`](../commands/#ready) up + themselves.

**`Default: 0`** +6. _Optional_
The minimum number of spectators that must be [`!ready`](../commands/#ready) for the game to + begin.

**`Default: 0`** +7. _Optional_
Whether to skip the veto phase. When skipping veto, `map_sides` determines sides, and if `map_sides` is + not set, sides are determined by `side_type`.

**`Default: false`** 8. A player's :material-steam: Steam ID. This can be in any format, but we recommend a string representation of SteamID 64, i.e. `"76561197987713664"`. -9. Players are represented each with a mapping of `Get5PlayerSteamID -> PlayerName` as a key-value dictionary. The name - is optional and should be set to an empty string to let players decide their own name. -10. _Optional_ - The spectators to allow into the game. If not defined, spectators cannot join the - game. **`Default: undefined`** -11. _Optional_ - The team that vetoes first. **`Default: team1`** -12. _Optional_ - The method used to determine sides. `standard` means that the team that doesn't pick a map gets the - side choice. `always_knife` means that sides are always determined by a knife-round and `never_kninfe` means that - `team1` always starts on CT. **`Default: standard`** -13. _Required_ - The map pool to pick from, as an array of strings (`["de_dust2", "de_nuke"]` etc.), or if `skip_veto` +9. Players are represented each with a mapping of `SteamID -> PlayerName` as a key-value dictionary. The name + is optional and should be set to an empty string to let players decide their own name. You can also provide a simple + string array of `SteamID` disable name-locking. +10. _Optional_
The spectators to allow into the game. If not defined, spectators cannot join the + game.

**`Default: undefined`** +11. _Optional_
The team that vetoes first.

**`Default: "team1"`** +12. _Optional_
The method used to determine sides when vetoing **or** if veto is disabled and `map_sides` are not + set.

`standard` means that the team that doesn't pick a map gets the side choice (only if `skip_veto` + is `false`).

`always_knife` means that sides are always determined by a knife-round.

`never_knife` + means that `team1` always starts on CT.

This parameter is ignored if `map_sides` is set for all + maps. `standard` and `always_knife` behave similarly when `skip_veto` is `true`.

**`Default: "standard"`** +13. _Required_
The map pool to pick from, as an array of strings (`["de_dust2", "de_nuke"]` etc.), or if `skip_veto` is `true`, the order of maps played (limited by `num_maps`). **This should always be odd-sized if using the in-game veto system.** -14. _Optional_ - Wrapper for the server's `mp_teamprediction_pct`. This determines the chances of `team1` - winning. **`Default: 0`** -15. _Optional_ - Wrapper for the server's `mp_teamprediction_txt`. **`Default: ""`** -16. _Required_ - The team's name. Sets `mp_teamname_1` or `mp_teamname_2`. Printed frequently in chat. -17. _Optional_ - A short version of the team name, used in clan tags in-game ( - if [`get5_set_client_clan_tags`](../configuration#get5_set_client_clan_tags) is disabled). **`Default: ""`** -18. _Optional_ - The ISO-code to use for the in-game flag of the team. Must be a supported country, i.e. `FR`, `UK` - , `SE` etc. **`Default: ""`** -19. _Optional_ - The team logo (wraps `mp_teamlogo_1` or `mp_teamlogo_2`), which requires to be on a FastDL in order for - clients to see. **`Default: ""`** -20. _Required_ - The data for the first team. -21. _Required_ - The data for the second team. -22. _Optional_ - Various commands to execute on the server when loading the match configuration. This can be both +14. _Optional_
Wrapper for the server's `mp_teamprediction_pct`. This determines the chances of `team1` + winning.

**`Default: 0`** +15. _Optional_
Wrapper for the server's `mp_teamprediction_txt`.

**`Default: ""`** +16. _Optional_
The team's name. Sets `mp_teamname_1` or `mp_teamname_2`. Printed frequently in chat. If you don't + define a team name, it will be set to `team_` followed by the name of the captain, i.e. `team_s1mple`. +

**`Default: ""`** +17. _Optional_
A short version of the team name, used in clan tags in-game (requires + that [`get5_set_client_clan_tags`](../configuration#get5_set_client_clan_tags) is disabled). +

**`Default: ""`** +18. _Optional_
The ISO-code to use for the in-game flag of the team. Must be a supported country, i.e. `FR`,`UK`,`SE` + etc.

**`Default: ""`** +19. _Optional_
The team logo (wraps `mp_teamlogo_1` or `mp_teamlogo_2`), which requires to be on a FastDL in order + for clients to see.

**`Default: ""`** +20. _Required_
The data for the first team. +21. _Required_
The data for the second team. +22. _Optional_
Various commands to execute on the server when loading the match configuration. This can be both regular server-commands and any [`Get5 configuration parameter`](configuration.md), - i.e. `{"hostname": "Match #3123 - Astralis vs. NaVi"}`. **`Default: undefined`** -23. _Optional_ - Similarly to `players`, this object maps coaches using their Steam ID and - name. **`Default: undefined`** -24. _Required_ - The players on the team. -25. _Optional_ - Wrapper of the server's `mp_teammatchstat_txt` cvar, but can use `{MAPNUMBER}` and `{MAXMAPS}` as + i.e. `{"hostname": "Match #3123 - Astralis vs. NaVi"}`.

**`Default: undefined`** +23. _Optional_
Similarly to `players`, this object maps [coaches](coaching.md) using their Steam ID and + name, locking them to the coach slot unless removed using [`get5_removeplayer`](../commands/#get5_removeplayer). + Setting a Steam ID as coach takes precedence over being set as a player.

**`Default: undefined`** +24. _Required_
The players on the team. +25. _Optional_
Wrapper of the server's `mp_teammatchstat_txt` cvar, but can use `{MAPNUMBER}` and `{MAXMAPS}` as variables that get replaced with their integer values. In a BoX series, you probably don't want to set this since Get5 automatically sets `mp_teamscore` cvars for the current series score, and take the place of - the `mp_teammatchstat` cvars. **`Default: Map {MAPNUMBER} of {MAXMAPS}`** -26. _Optional_ - The current score in the series, this can be used to give a team a map advantage or used as a manual - backup method. **`Default: 0`** -27. _Optional_ - Wraps `mp_teammatchstat_1` and `mp_teammatchstat_2`. You probably don't want to set this, in BoX + the `mp_teammatchstat` cvars.

**`Default: "Map {MAPNUMBER} of {MAXMAPS}"`** +26. _Optional_
The current score in the series, this can be used to give a team a map advantage or used as a manual + backup method.

**`Default: 0`** +27. _Optional_
Wraps `mp_teammatchstat_1` and `mp_teammatchstat_2`. You probably don't want to set this, in BoX series, `mp_teamscore` cvars are automatically set and take the place of the `mp_teammatchstat_x` - cvars. **`Default: ""`** + cvars.

**`Default: ""`** 28. Match teams can also be loaded from a separate file, allowing you to easily re-use a match configuration for - different sets of teams. - A `fromfile` value could be `"addons/sourcemod/configs/get5/team_nip.json"`, and that file should contain a valid - `Get5MatchTeam` object. -29. _Optional_ - The name of the spectator team. **`Default: casters`** -30. _Optional_ - The spectator/caster Steam IDs and names. - -!!! warning "SteamID64 in `.cfg` files" - - You may have trouble using SteamID64 inside a KeyValue (`.cfg`) match config. The Valve KeyValue parser will - interpret any integer string as an integer (even if read as a string), and this value will - not fit inside a SourceMod-internal 32-bit cell. For `.cfg`, use the regular steamID, i.e. `STEAM_0:0:13723968`. - This is *not* a problem if you use the JSON format. Also, remember not to pass SteamID 64 as numbers, as they are - too large to reliably handle in JavaScript; always enclose them in quotes. - -#### Example - -```typescript title="JSON example with Node.js" -const match_schema: Match = { - "match_title": "Astralis vs. NaVi", - "matchid": "3123", - "num_maps": 3, - "players_per_team": 5, - "coaches_per_team": 2, - "min_players_to_ready": 2, - "min_spectators_to_ready": 0, - "skip_veto": false, - "veto_first": "team1", - "side_type": "always_knife", - "spectators": { + different sets of teams. A `fromfile` value could be `"addons/sourcemod/configs/get5/team_nip.json"`, and is always + relative to the `csgo` directory. The file should contain a valid `Get5MatchTeam` object. Note that the file you + point to must be in the same format as the main file, so pointing to a `.cfg` file when the main file is `.json` + will **not** work. +29. _Optional_
The name of the spectator team.

**`Default: "casters"`** +30. _Optional_
The spectator/caster Steam IDs and names. +31. _Optional_
Determines the starting sides for each map. If this array is shorter than `num_maps`, `side_type` will + determine the side-behavior of the remaining maps. Ignored if `skip_veto` is `false`. +

**`Default: undefined`** +32. _Optional_
If `false`, the entire map list will be played, regardless of score. If `true`, a series will be won + when the series score for a team exceeds the number of maps divided by two.

**`Default: true`** + +## Examples {: #example } + +These examples are identical in the way they would work if loaded. + +=== "JSON (recommended)" + + !!! tip "Example only" + + `map_sides` would only work with `skip_veto: true`. + + ```json title="addons/sourcemod/get5/astralis_vs_navi_3123.json" + { + "match_title": "Astralis vs. NaVi", + "matchid": "3123", + "clinch_series": true, + "num_maps": 3, + "players_per_team": 5, + "coaches_per_team": 2, + "min_players_to_ready": 2, + "min_spectators_to_ready": 0, + "skip_veto": false, + "veto_first": "team1", + "side_type": "standard", + "spectators": { "name": "Blast PRO 2021", "players": { - "76561197987511774": "Anders Blume" + "76561197987511774": "Anders Blume" } - }, - "map_list": ["de_dust2", "de_nuke", "de_inferno", "de_mirage", "de_vertigo", "de_ancient", "de_overpass"], - "team1": { - "name": "Natus Vincere", - "tag": "NaVi", - "flag": "UA", - "logo": "nv", - "players": { - "76561198034202275": "s1mple", - "76561198044045107": "electronic", - "76561198246607476": "b1t", - "76561198121220486": "Perfecto", - "76561198040577200": "sdy" - }, - "coaches": { - "76561198013523865": "B1ad3" - } - }, - "team2": { + }, + "maplist": [ + "de_dust2", + "de_nuke", + "de_inferno", + "de_mirage", + "de_vertigo", + "de_ancient", + "de_overpass" + ], + "map_sides": [ + "team1_ct", + "team2_ct", + "knife" + ], + "team1": { + "fromfile": "addons/sourcemod/get5/team_navi.json" + }, + "team2": { "name": "Astralis", "tag": "Astralis", "flag": "DK", - "logo": "as", + "logo": "astr", "players": { - "76561197990682262": "Xyp9x", - "76561198010511021": "gla1ve", - "76561197979669175": "K0nfig", - "76561198028458803": "BlameF", - "76561198024248129": "farlig" + "76561197990682262": "Xyp9x", + "76561198010511021": "gla1ve", + "76561197979669175": "K0nfig", + "76561198028458803": "BlameF", + "76561198024248129": "farlig" }, "coaches": { - "76561197987144812": "Trace" + "76561197987144812": "Trace" } - }, - "cvars": { + }, + "cvars": { "hostname": "Get5 Match #3123", "mp_friendly_fire": "0", "get5_end_match_on_empty_server": "0", - "get5_stop_command_enabled": "0" + "get5_stop_command_enabled": "0", + "sm_practicemode_can_be_started": "0" + } } -} + ``` + `fromfile` example: + ```json title="addons/sourcemod/get5/team_navi.json" + { + "name": "Natus Vincere", + "tag": "NaVi", + "flag": "UA", + "logo": "navi", + "players": { + "76561198034202275": "s1mple", + "76561198044045107": "electronic", + "76561198246607476": "b1t", + "76561198121220486": "Perfecto", + "76561198040577200": "sdy" + }, + "coaches": { + "76561198013523865": "B1ad3" + } + } + ``` -// And the config file could be placed on the server like this: -const json = JSON.stringify(match_schema); -fs.writeFileSync('addons/sourcemod/get5/astralis_vs_navi_3123.json', json); -``` + And in TypeScript, using the above interface definition file: + ```typescript title="Typescript JSON example with Node.js" + const match_schema: Get5Match = { + "match_title": "Astralis vs. NaVi", + "matchid": "3123", + "clinch_series": true, + "num_maps": 3, + "players_per_team": 5, + "coaches_per_team": 2, + "min_players_to_ready": 2, + "min_spectators_to_ready": 0, + "skip_veto": false, + "veto_first": "team1", + "side_type": "standard", + "spectators": { + "name": "Blast PRO 2021", + "players": { + "76561197987511774": "Anders Blume" + } + }, + "maplist": ["de_dust2", "de_nuke", "de_inferno", "de_mirage", "de_vertigo", "de_ancient", "de_overpass"], + "map_sides": ["team1_ct", "team2_ct", "knife"], // Example; would only work with "skip_veto": true + "team1": { + "fromfile": "addons/sourcemod/get5/team_navi.json" + }, + "team2": { + "name": "Astralis", + "tag": "Astralis", + "flag": "DK", + "logo": "astr", + "players": { + "76561197990682262": "Xyp9x", + "76561198010511021": "gla1ve", + "76561197979669175": "K0nfig", + "76561198028458803": "BlameF", + "76561198024248129": "farlig" + }, + "coaches": { + "76561197987144812": "Trace" + } + }, + "cvars": { + "hostname": "Get5 Match #3123", + "mp_friendly_fire": "0", + "get5_end_match_on_empty_server": "0", + "get5_stop_command_enabled": "0", + "sm_practicemode_can_be_started": "0" + } + } + + // And the config file could be placed on the server like this: + const json = JSON.stringify(match_schema); + fs.writeFileSync('addons/sourcemod/get5/astralis_vs_navi_3123.json', json); + ``` + +=== "KeyValue" + + !!! warning "All strings, no brakes" + + Note that `false` does not exist in the KeyValue format and that all numerical values are wrapped in quotes. The + empty strings as values in dictionaries (`maplist` and `map_sides`) are also required. + + ```cfg title="addons/sourcemod/get5/astralis_vs_navi_3123.cfg" + "Match" + { + "match_title" "Astralis vs. NaVi" + "matchid" "3123" + "clinch_series" "1" + "num_maps" "3" + "players_per_team" "5" + "coaches_per_team" "2" + "min_players_to_ready" "2" + "min_spectators_to_ready" "0" + "skip_veto" "0" + "veto_first" "team1" + "side_type" "standard" + "spectators" + { + "name" "Blast PRO 2021" + "players" + { + "76561197987511774" "Anders Blume" + } + } + "maplist" + { + "de_dust2" "" + "de_nuke" "" + "de_inferno" "" + "de_mirage" "" + "de_vertigo" "" + "de_ancient" "" + "de_overpass" "" + } + "map_sides" // Example; would only work with "skip_veto" "1" + { + "team1_ct" "" + "team2_ct" "" + "knife" "" + } + "team1" + { + "fromfile" "addons/sourcemod/get5/team_navi.cfg" + } + "team2" + { + "name" "Astralis" + "tag" "Astralis" + "flag" "DK" + "logo" "astr" + "players" + { + "76561197990682262" "Xyp9x" + "76561198010511021" "gla1ve" + "76561197979669175" "K0nfig" + "76561198028458803" "BlameF" + "76561198024248129" "farlig" + } + "coaches" + { + "76561197987144812" "Trace" + } + } + "cvars" + { + "hostname" "Get5 Match #3123" + "mp_friendly_fire" "0" + "get5_end_match_on_empty_server" "0" + "get5_stop_command_enabled" "0" + "sm_practicemode_can_be_started" "0" + } + } + ``` + `fromfile` example: + ```cfg title="addons/sourcemod/get5/team_navi.cfg" + { + "name" "Natus Vincere" + "tag" "NaVi" + "flag" "UA" + "logo" "navi" + "players" + { + "76561198034202275" "s1mple" + "76561198044045107" "electronic" + "76561198246607476" "b1t" + "76561198121220486" "Perfecto" + "76561198040577200" "sdy" + } + "coaches" + { + "76561198013523865" "B1ad3" + } + } + ``` diff --git a/documentation/docs/pausing.md b/documentation/docs/pausing.md index 5acb12ce8..841e9f306 100644 --- a/documentation/docs/pausing.md +++ b/documentation/docs/pausing.md @@ -35,9 +35,15 @@ Administrators cannot call technical pauses, as an administrative pause will be triggered instead. You can set [the maximum number of technical pauses](../configuration/#get5_max_tech_pauses). -## :material-account-hard-hat-outline: Administrative {: #dministrative } +## :material-backup-restore: Backup {: #backup } -As a [server admin](../installation/#administrators), you can pause the match at any time and with no time +If the game is [restored from a backup](backup.md), it will be so in a paused state. Both teams must +[`!unpause`](../commands/#unpause) before the match can continue. Administrators can also unpause backup pauses, or even +override them to an [administrative pause](#administrative). + +## :material-account-hard-hat-outline: Administrative {: #administrative } + +As a server admin, you can pause the match at any time and with no time restrictions, but you **cannot** use [`mp_pause_match`](https://totalcsgo.com/command/mppausematch) (or its unpause equivalent) at any stage. Due to the way Get5 handles pausing, you must use `sm_pause` in the console, since this will track all details and configurations related to pausing in the system. Similarly, `sm_unpause` must be used to unpause. @@ -45,13 +51,7 @@ Pauses initiated by administrators via console **cannot** be [`!unpause`'ed](../ that an [`admin` pause event](events_and_forwards.md) is fired when the game is paused during veto (only if [`get5_pause_on_veto`](../configuration/#get5_pause_on_veto) is enabled). -!!! fail "I'm an admin on my server, but I cannot call admin pauses!" +!!! question "I'm an admin on my server, but I cannot call admin pause?" Only console/RCON is considered an administrator in pause-context. Having an admin flag as a user/player does not allow you to call administrative pauses. - -!!! help "But the [event system](events_and_forwards.md) also hints at the existence of a `backup` pause type?" - - Internally, Get5 uses the `backup` pause type when pausing due to loading backup configurations. To not confuse - any program reaction to pauses, we added a special pause type in these cases, and you should probably just ignore - these events. They still fire because *technically* the game is pausing. diff --git a/documentation/docs/stats_system.md b/documentation/docs/stats_system.md index e9105cdc8..46c662e8f 100644 --- a/documentation/docs/stats_system.md +++ b/documentation/docs/stats_system.md @@ -1,30 +1,31 @@ -# Player Stats System +# :material-chart-bar: Player Stats System -When a get5 match is live, the plugin will automatically record match stats for each player, across each map in the -match. These are recorded in an internal KeyValues structure, and are available at any time during the match (including -the postgame waiting period) via the `Get5_GetMatchStats` native and -the [`get5_dumpstats`](./commands.md#serveradmin-commands) command. +!!! warning -Note: the stats collection is not going to be reliable if -using [`get5_check_auths 0`](./configuration.md#server-setup). + None of the methods for collecting stats are going to be reliable if + [`get5_check_auths`](../configuration/#get5_check_auths) is set to `0`. -## SourceMod Forwards +## SourceMod Forwards {: #forwards } If you're writing your own plugin, you can collect stats from the game using the [forwards](./events_and_forwards.md) provided by Get5. -## Stats KeyValues structure +## KeyValue System {: #keyvalue } -The root level of the KV contains data for the full series: the series winner (if one exists yet) and the series type ( -bo1, bo2..., etc). +Get5 will automatically record basic stats for each player for each map in the match. These are stored in an internal +KeyValues structure, and are available at any time during the match (including the postgame waiting period) via the +`Get5_GetMatchStats` native and the [`get5_dumpstats`](../commands/#get5_dumpstats) command. -Under that root level, there is a level for each map ("map1", "map2"), which contains the map winner (if one exists yet) -, the mapname, and the demo file recording. +The root level contains data for the full series; the series winner (if one exists yet) and the series type ( +bo1, bo3, etc). -Under the map level, there is a section for each team ("team1" and "team2) which contains the current team score (on +Under the root level is a level for each map (`map0`, `map1` etc.), which contains the map winner (if one exists yet), +the map name and the demo file recording. + +Under the map level is a section for each team (`team1` and `team2`), which contains the current team score (on that map) and the team name. -Each player has a section under the team level under the section name of their steam64id. It contains all the personal +Each player has a section under the team level under the section name of their SteamID 64. It contains all the personal level stats: name, kills, deaths, assists, etc. Partial Example: @@ -32,12 +33,13 @@ Partial Example: ``` "Stats" { - "series_type" "bo1" + "series_type" "bo1" "team1_name" "EnvyUs" "team2_name" "Fnatic" "map0" { "mapname" "de_mirage" + "demo_filename" "304_map1_de_mirage.dem" "winner" "team1" "team1" { @@ -46,35 +48,78 @@ Partial Example: { "name" "xyz" "kills" "0" - "deaths" "1" - "assists" "5" - "damage" "352" + "deaths" "1" + "assists" "5" + "damage" "352" } } } } ``` -## What Stats Are Collected +!!! question "What stats are collected?" -See the [get5 include](https://github.com/splewis/get5/blob/master/scripting/include/get5.inc#L1769) for what stats will -be recorded and what their key in the KeyValue structure is. + See the [get5.inc include file](https://github.com/splewis/get5/blob/master/scripting/include/get5.inc#L1769) for + what stats will be recorded and what their keys are in the KeyValue structure. ## MySQL Statistics {: #mysql } Get5 ships with a (disabled by default) plugin called `get5_mysqlstats` that will save many of the stats to a MySQL -database. To use this: +database. You can use the included plugin as a source of inspiration and build your own to collect even more stats, or +even wrap a website around it for managing matches. The included plugin is meant as a proof-of-concept of this +functionality, but can also be used as-is. + +!!! danger "Fixed Match IDs" + + If you use the MySQL extension, you should **not** set the `matchid` in your + [match configuration](../match_schema/#schema) (just leave it empty) or when creating scrims or matches using the + [`get5_scrim`](../commands/#get5_scrim) or [`get5_creatematch`](../commands/#get5_creatematch) commands. The match + ID will be set to the + [auto-incrementing integer](https://dev.mysql.com/doc/refman/8.0/en/example-auto-increment.html) (cast to a string) + returned by inserting into the `get5_stats_matches` table. + +!!! tip "Advanced users only" -- Create the tables using this [schema](https://github.com/splewis/get5/blob/master/misc/import_stats.sql), raw text - link can be found [here](https://raw.githubusercontent.com/splewis/get5/master/misc/import_stats.sql). -- Configure a `"get5"` database section in `addons/sourcemod/configs/databases.cfg`. -- Make sure the `get5_mysqlstats` plugin is enabled (moved up a directory from `addons/sourcemod/plugins/disabled` - directory). + You should have a basic understanding of MySQL if you wish to use this plugin. It is assumed you know what the + commands below do. -**Note**: If you use this module, you can force the match ID used by setting it in your match config -(the [Match Schema](../match_schema/#optional-values) section). If you don't do this, the match ID will be set to the -auto-incrementing integer (cast to a string) returned by inserting into the `get5_stats_matches` table. It is strongly -recommended that you always leave the `matchid` blank, as MySQL will then manage the IDs for you. +1. Make sure the `get5_mysqlstats.smx` plugin is enabled (moved up a directory from `addons/sourcemod/plugins/disabled` + directory). -If you are using an external web panel, **this plugin is not needed** as most external applications record to their own -match tables. +2. Have a MySQL server reachable from the game server's network. These commands are for MySQL 8 but should also work on +MySQL 5.7. + +3. Create a schema/database for your tables: +```mysql +CREATE SCHEMA `get5` DEFAULT CHARACTER SET `utf8mb4` COLLATE `utf8mb4_0900_ai_ci`; +USE `get5`; +``` + :warning: The `utf8mb4` part ensures that your database can handle all kinds of emojis and unicode characters. This is + the default in MySQL 8 but must be explicitly defined for MySQL 5.7. + +4. Configure a database user and grant it access to the database: +```mysql +CREATE USER 'get5_db_user'@'%' IDENTIFIED WITH mysql_native_password BY 'super_secret_password'; +GRANT ALL ON `get5`.* TO 'get5_db_user'@'%'; +``` + :warning: You **can** use the `root` database user instead if you wish. `@'%'` means that the user can log in from any + network location, and you can replace this with `@'localhost'` if your database is running on the same host as the + game server. + +5. Create the required tables using [these commands](https://github.com/splewis/get5/blob/master/misc/import_stats.sql). +Raw text link can be found [here](https://raw.githubusercontent.com/splewis/get5/master/misc/import_stats.sql). + +6. Configure a `"get5"` database section in SourceMod and provide the parameters you used to configure your database: +!!! example ":material-file-cog: `addons/sourcemod/configs/databases.cfg`" + + ``` + "get5" + { + "driver" "mysql" + "host" "127.0.0.1" + "database" "get5" + "user" "get5_db_user" + "pass" "super_secret_password" + "port" "3306" + } + ``` diff --git a/documentation/docs/translations.md b/documentation/docs/translations.md index 84ee36105..544d31052 100644 --- a/documentation/docs/translations.md +++ b/documentation/docs/translations.md @@ -2,14 +2,14 @@ Get5 has been translated into a few languages, but some a are still incomplete or could use a grammatical hand. If you are proficient in a language other than English, you are welcome to open a pull request on GitHub with adjustments or -even entirely new languages. Note that you should be **good** at the language; machine-translations or sloppy linguistics -are worse than defaulting Get5 to English. If you cannot code and have found errors in translations, feel free to join -the [Discord](../community/#discord) and let us know. +even entirely new languages. Note that you should be **good** at the language; machine-translations or sloppy +linguistics are worse than defaulting Get5 to English. If you cannot code and have found errors in translations, feel +free to join the [Discord](../community/#discord) and let us know. ## How to translate? Inside the `translations` folder you will find the base `get5.phrases.txt` file which is the English one and the -fallback in case a translation string cannot be found in a client's language. This is the "single source of truth" and +fallback in case a translation string cannot be found in a client's language. This is the _single source of truth_ and should be used when translating. Each language has a folder (for instance `fr` for french) within which there is another `get5.phrases.txt` file, but in French. @@ -18,44 +18,56 @@ entire language file**. ## Example -```yaml -"TeamPickedMapInfoMessage" -{ - "#format" "{1:s},{2:s},{3:d}" # (1) - "en" "{1} picked {GREEN}{2} {NORMAL}as map {3}." # (2) -} -``` +!!! example "translations/get5.phrases.txt" -1. The `#format` parameter indicates the order and types of parameters. These will *not* be defined in other languages, and -you should only provide the language string itself (with its language prefix, i.e. `en`). The original file indicates -what `{1}`, `{2}` and `{3}` are. In this case, the first and second arguments are strings and the third is a number. -2. Use the English strings and the [reference](#reference) below to determine how to translate the string. + ```yaml + "TeamPickedMapInfoMessage" + { + "#format" "{1:s},{2:s},{3:d}" # (1) + "en" "{1} picked {2} as map {3}." # (2) + } + ``` + + 1. The `#format` parameter indicates the order and types of parameters. These will *not* be defined in other + languages, and you should only provide the language string itself (with its language prefix, i.e. `en`). The + original file indicates what `{1}`, `{2}` and `{3}` are. In this case, the first and second arguments are strings + and the third is a number. + 2. Use the English strings and the [reference](#reference) below to determine how to translate the string. As the string implies, this example is used when a team picks a map, and the output is printed to chat and looks like -this: `Team A picked de_dust as map 2.` +this: `Team A picked de_dust2 as map 2.` The French translation file for this string looks like this: + +!!! example "translations/fr/get5.phrases.txt" + + ```yaml + "TeamPickedMapInfoMessage" + { + "fr" "{1} a choisi {2} comme map {3}." + } + ``` ## Types of strings ####`Chat` : Displayed in the regular game chat. This is the only type that supports - [color modifiers](../configuration#color-substitutes). +[color modifiers](../configuration#color-substitutes). You should use the same colors in the same lexical context as the +English translation. All injected variables are colored automatically if required. ####`HintText` : Displayed as a ["hint"](https://sourcemod.dev/#/halflife/function.PrintHintText) in the lower center of the screen, - where you would also see the pause or restart alert. +where you would also see the pause or restart alert. ####`KickedNote` : Displayed as a modal in the middle of the CS:GO menu after you have been removed from the server. These must **not** - end with a full stop as this is added automatically. +end with a full stop as this is added automatically. ####`Menu` : Displayed as an in-game menu where you select/browse using the numbers on your keyboard. - ## String Reference {: #reference } !!! warning @@ -66,15 +78,15 @@ this: `Team A picked de_dust as map 2.` | String | Example | Type | |---------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------| -| `WaitingForCastersReadyInfoMessage` | Waiting for _Team A_ to type !ready to being. | Chat | -| `ReadyToVetoInfoMessage` | Type !ready when your team is ready to veto. | Chat | -| `ReadyToRestoreBackupInfoMessage` | Type !ready when you are ready to restore the match backup. | Chat | -| `ReadyToKnifeInfoMessage` | Type !ready when you are ready to knife. | Chat | -| `ReadyToStartInfoMessage` | Type !ready when you are ready to begin. | Chat | +| `WaitingForCastersReadyInfoMessage` | Waiting for _Team A_ to type _!ready_ to begin. | Chat | +| `ReadyToVetoInfoMessage` | Type _!ready_ when your team is ready to veto. | Chat | +| `ReadyToRestoreBackupInfoMessage` | Type _!ready_ when you are ready to restore the match backup. | Chat | +| `ReadyToKnifeInfoMessage` | Type _!ready_ when you are ready to knife. | Chat | +| `ReadyToStartInfoMessage` | Type _!ready_ when you are ready to begin. | Chat | | `YouAreReady` | You have been marked as ready. | Chat | -| `YouAreReadyAuto` | NOTE: You have been marked as ready due to game activity. Type !unready if you are not ready. | HintText | +| `YouAreReadyAuto` | NOTE: You have been marked as ready due to game activity. Type _!unready_ if you are not ready. | HintText | | `YouAreNotReady` | You have been marked as NOT ready. | Chat | -| `WaitingForEnemySwapInfoMessage` | _Team A_ won the knife round. Waiting for them to type !stay or !swap. | Chat | +| `WaitingForEnemySwapInfoMessage` | _Team A_ won the knife round. Waiting for them to type _!stay_ or _!swap_. | Chat | | `WaitingForGOTVBrodcastEndingInfoMessage` | The map will change once the GOTV broadcast has ended. | Chat | | `WaitingForGOTVVetoInfoMessage` | The map will change once the GOTV broadcast has displayed the map vetoes. | Chat | | `NoMatchSetupInfoMessage` | No match was set up | KickedNote | @@ -92,6 +104,7 @@ this: `Team A picked de_dust as map 2.` | `TacticalPauseMidSentence` | _Team A_ (_CT_) __tactical pause__ (_1_/_2_). | HintText | | `TimeRemainingBeforeAnyoneCanUnpausePrefix` | _Team A_ (_CT_) technical pause (_1_/_2_). __Time remaining before anyone can unpause__: _2:30_ | HintText | | `StopCommandNotEnabled` | The stop command is not enabled. | Chat | +| `StopCommandVotingReset` | The request by _Team A_ to stop the game was canceled as the round ended. | Chat | | `PauseTimeRemainingPrefix` | _Team A_ (_CT_) tactical pause. __Remaining pause time__: _2:15_ | HintText | | `PausedForBackup` | The game was restored from a backup. Both teams must unpause to continue. | HintText | | `AwaitingUnpause` | _Team A_ (_CT_) tactical pause. __Awaiting unpause__. | HintText | @@ -100,12 +113,12 @@ this: `Team A picked de_dust as map 2.` | `UserCannotUnpauseAdmin` | As an admin has called for this pause, it must also be unpaused by an admin. | Chat | | `PausingTeamCannotUnpauseUntilFreezeTime` | You cannot unpause before your pause has started. Pause requests cannot be canceled. | Chat | | `PauseRunoutInfoMessage` | _Team A_ has run out of pause time. Unpausing the match. | Chat | -| `TechPauseRunoutInfoMessage` | Maximum technical pause length has been reached. Anyone may unpause now. | Chat | +| `TechPauseRunoutInfoMessage` | Maximum technical pause length has been reached. Anyone may _!unpause_ now. | Chat | | `TechPauseNoTimeRemaining` | _Team A_ has no more tech pause time. Please use tactical pauses. | Chat | | `TechPauseNoPausesRemaining` | _Team B_ has no more tech pauses. Please use tactical pauses. | Chat | | `TechPausePausesRemaining` | Technical pauses remaining for _Team A_: _2_ | Chat | | `MatchUnpauseInfoMessage` | _PlayerName_ unpaused the match. | Chat | -| `WaitingForUnpauseInfoMessage` | _Team A_ wants to unpause, waiting for Team B to type !unpause. | Chat | +| `WaitingForUnpauseInfoMessage` | _Team A_ wants to unpause, waiting for Team B to type _!unpause_. | Chat | | `PausesLeftInfoMessage` | Tactical pauses remaining for _Team A_: _3_ | Chat | | `TeamFailToReadyMinPlayerCheck` | You must have at least _3_ player(s) on the server to ready up. | Chat | | `TeamReadyToVetoInfoMessage` | _Team A_ is ready to veto. | Chat | @@ -113,27 +126,26 @@ this: `Team A picked de_dust as map 2.` | `TeamReadyToKnifeInfoMessage` | _Team A_ is ready to knife for sides. | Chat | | `TeamReadyToBeginInfoMessage` | _Team A_ is ready to begin the match. | Chat | | `TeamNotReadyInfoMessage` | _Team A_ is no longer ready. | Chat | -| `ForceReadyInfoMessage` | You may type !forceready to force ready your team if you have less than _5_ players. | Chat | +| `ForceReadyInfoMessage` | You may type !forceready to force-ready your team if you have less than _5_ players. | Chat | | `TeammateForceReadied` | Your team was force-readied by _PlayerName_. | Chat | | `AdminForceReadyInfoMessage` | An admin has force-readied all teams. | Chat | -| `AdminForceEndInfoMessage` | An admin force ended the match. | Chat | -| `AdminForcePauseInfoMessage` | An admin force paused the match. | Chat | -| `AdminForceUnPauseInfoMessage` | An admin force unpaused the match. | Chat | -| `TeamWantsToReloadLastRoundInfoMessage` | _Team A_ wants to stop and reload last round, need _Team B_ to confirm with !stop. | Chat | +| `AdminForceEndInfoMessage` | An admin force-ended the match. | Chat | +| `AdminForceEndWithWinnerInfoMessage` | An admin force-ended the match, setting _Team 1_ as the winner. | Chat | +| `AdminForcePauseInfoMessage` | An admin force-paused the match. | Chat | +| `AdminForceUnPauseInfoMessage` | An admin unpaused the match. | Chat | +| `TeamWantsToReloadCurrentRound` | _Team A_ wants to restore the game to the beginning of the current round. _Team B_ must confirm with _!stop_. | Chat | | `TeamWinningSeriesInfoMessage` | _Team A_ is winning the series _2_-_1_. | Chat | | `SeriesTiedInfoMessage` | The series is tied at _1_-_1_. | Chat | -| `NextSeriesMapInfoMessage` | The next map in the series is _de_nuke_. | Chat | +| `NextSeriesMapInfoMessage` | The next map in the series is _de_nuke_ and it will start in _1:30_. | Chat | | `TeamWonMatchInfoMessage` | _Team A_ has won the match. | Chat | | `TeamTiedMatchInfoMessage` | _Team A_ and _Team B_ have tied the match. | Chat | -| `TeamsSplitSeriesBO2InfoMessage` | _Team A_ and _Team B_ have split the series 1-1. | Chat | | `TeamWonSeriesInfoMessage` | _Team A_ has won the series _2_-_1_. | Chat | | `MatchFinishedInfoMessage` | The match is over | KickedNote | -| `CurrentScoreInfoMessage` | _Team A_ _12_ - _Team B_ _8_ | Chat | | `BackupLoadedInfoMessage` | Successfully loaded backup _backup_file_03.cfg_. | Chat | | `MatchBeginInSecondsInfoMessage` | The match will begin in _3_ seconds. | Chat | -| `MatchIsLiveInfoMessage` | Match is LIVE | Chat | +| `MatchIsLiveInfoMessage` | Match is LIVE
Match is LIVE
Match is LIVE
Match is LIVE
Match is LIVE | Chat | | `KnifeIn5SecInfoMessage` | The knife round will begin in 5 seconds. | Chat | -| `KnifeInfoMessage` | Knife! | Chat | +| `KnifeInfoMessage` | Knife!
Knife!
Knife!
Knife!
Knife! | Chat | | `TeamDecidedToStayInfoMessage` | _Team A_ has decided to stay. | Chat | | `TeamDecidedToSwapInfoMessage` | _Team A_ has decided to swap. | Chat | | `TeamLostTimeToDecideInfoMessage` | _Team A_ will stay since they did not make a decision in time. | Chat | @@ -144,12 +156,16 @@ this: `Team A picked de_dust as map 2.` | `TeamSelectSideInfoMessage` | _Team A_ has selected to start on _CT_ on _de_nuke_. | Chat | | `TeamVetoedMapInfoMessage` | _Team A_ vetoed _de_nuke_. | Chat | | `CaptainLeftOnVetoInfoMessage` | A captain left during the veto, pausing the veto. | Chat | -| `ReadyToResumeVetoInfoMessage` | Type !ready when you are ready to resume the veto. | Chat | +| `ReadyToResumeVetoInfoMessage` | Type _!ready_ when you are ready to resume the veto. | Chat | | `MatchConfigLoadedInfoMessage` | Loaded match config. | Chat | -| `MoveToCoachInfoMessage` | You were moved to the coach position because your team is full. | Chat | +| `MoveToCoachInfoMessage` | You were moved to the coach position as your team is full. | Chat | +| `CannotLeaveCoachingTeamIsFull` | You cannot leave the coach position as your team is full. | Chat | +| `CoachingNotEnabled` | Coaching is not enabled. You must set _sv_coaching_enabled_ to 1. | Chat | +| `PlayerIsCoachingTeam` | _PlayerName_ is coaching _Team A_. | Chat | +| `CanOnlyCoachDuringWarmup` | You can only change to or from coach during warmup. | Chat | +| `AllCoachSlotsFilledForTeam` | All coach slots (_2_) are currently filled for your team. | Chat | | `ReadyTag` | **[READY]** PlayerName: Hey, I'm ready... | Chat | | `NotReadyTag` | **[NOT READY]** PlayerName: Hey, I'm not ready... | Chat | -| `MatchPoweredBy` | Powered by Get5 | Chat | | `MapVetoPickMenuText` | Select a map to PLAY: | Menu | | `MapVetoPickConfirmMenuText` | Confirm you want to PLAY _de_nuke_: | Menu | | `MapVetoBanMenuText` | Select a map to VETO: | Menu | @@ -161,3 +177,19 @@ this: `Team A picked de_dust as map 2.` | `VetoCountdown` | Veto commencing in _3_ seconds. | Chat | | `NewVersionAvailable` | A newer version of Get5 is available. Please visit _splewis.github.io/get5_ to update. | Chat | | `PrereleaseVersionWarning` | You are running an unofficial version of Get5 (_0.9.0-c7af39a_) intended for development and testing only. This message can be disabled with _get5_print_update_notice_. | Chat | + +## Supported Languages {: #supported-languages } + +These are the languages Get5 supports. The links will take you to the source translation file for the language on +GitHub. Most languages are incomplete, and if a translation string is missing, the English default will be used. + +#### :flag_gb: [English](https://github.com/splewis/get5/blob/development/translations/get5.phrases.txt) (default) {: #en } +#### :flag_fr: [French](https://github.com/splewis/get5/tree/development/translations/fr/get5.phrases.txt) {: #fr } +#### :flag_de: [German](https://github.com/splewis/get5/tree/development/translations/de/get5.phrases.txt) {: #de } +#### :flag_es: [Spanish](https://github.com/splewis/get5/tree/development/translations/es/get5.phrases.txt) {: #es } +#### :flag_cn: [Chinese](https://github.com/splewis/get5/tree/development/translations/chi/get5.phrases.txt) {: #cn } +#### :flag_dk: [Danish](https://github.com/splewis/get5/tree/development/translations/da/get5.phrases.txt) {: #da } +#### :flag_hu: [Hungarian](https://github.com/splewis/get5/tree/development/translations/hu/get5.phrases.txt) {: #hu } +#### :flag_pl: [Polish](https://github.com/splewis/get5/tree/development/translations/pl/get5.phrases.txt) {: #pl } +#### :flag_pt: [Portuguese](https://github.com/splewis/get5/tree/development/translations/pt/get5.phrases.txt) {: #pt } +#### :flag_ru: [Russian](https://github.com/splewis/get5/tree/development/translations/ru/get5.phrases.txt) {: #ru } diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 483d02a21..24eb9f4f2 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -35,10 +35,12 @@ nav: - Configuration: configuration.md - Basics: - Getting Started: getting_started.md + - Match Schema: match_schema.md - Commands: commands.md + - Coaching: coaching.md - Pausing: pausing.md - Backup System: backup.md - - Match Schema: match_schema.md + - GOTV & Demos: gotv.md - Advanced: - Developer API: developer_api.md - Events & Forwards: events_and_forwards.md @@ -57,6 +59,8 @@ markdown_extensions: - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true - pymdownx.highlight: anchor_linenums: true - pymdownx.emoji: diff --git a/scripting/get5.sp b/scripting/get5.sp index 7ab43460c..de01988d8 100644 --- a/scripting/get5.sp +++ b/scripting/get5.sp @@ -23,11 +23,11 @@ #include "include/restorecvars.inc" #include #include // github.com/clugg/sm-json +#include #include #include #include #include -#include #undef REQUIRE_EXTENSIONS #include @@ -38,17 +38,15 @@ #define DEBUG_CVAR "get5_debug" #define MATCH_ID_LENGTH 64 #define MAX_CVAR_LENGTH 128 -#define MATCH_END_DELAY_AFTER_TV 10 +#define MATCH_END_DELAY_AFTER_TV 15 -#define TEAM1_COLOR "{LIGHT_GREEN}" -#define TEAM2_COLOR "{PINK}" #define TEAM1_STARTING_SIDE CS_TEAM_CT #define TEAM2_STARTING_SIDE CS_TEAM_T -#define KNIFE_CONFIG "get5/knife.cfg" #define DEFAULT_TAG "[{YELLOW}Get5{NORMAL}]" #if !defined LATEST_VERSION_URL -#define LATEST_VERSION_URL "https://raw.githubusercontent.com/splewis/get5/master/scripting/get5/version.sp" +#define LATEST_VERSION_URL \ + "https://raw.githubusercontent.com/splewis/get5/master/scripting/get5/version.sp" #endif #if !defined GET5_GITHUB_PAGE @@ -77,6 +75,8 @@ ConVar g_FixedPauseTimeCvar; ConVar g_KickClientImmunityCvar; ConVar g_KickClientsWithNoMatchCvar; ConVar g_LiveCfgCvar; +ConVar g_WarmupCfgCvar; +ConVar g_KnifeCfgCvar; ConVar g_LiveCountdownTimeCvar; ConVar g_MaxBackupAgeCvar; ConVar g_MaxTacticalPausesCvar; @@ -97,8 +97,12 @@ ConVar g_TeamTimeToStartCvar; ConVar g_TimeFormatCvar; ConVar g_VetoConfirmationTimeCvar; ConVar g_VetoCountdownCvar; -ConVar g_WarmupCfgCvar; ConVar g_PrintUpdateNoticeCvar; +ConVar g_RoundBackupPathCvar; +ConVar g_PhaseAnnouncementCountCvar; +ConVar g_Team1NameColorCvar; +ConVar g_Team2NameColorCvar; +ConVar g_SpecNameColorCvar; // Autoset convars (not meant for users to set) ConVar g_GameStateCvar; @@ -110,11 +114,12 @@ ConVar g_CoachingEnabledCvar; /** Series config game-state **/ int g_MapsToWin = 1; // Maps needed to win the series. -bool g_BO2Match = false; +bool g_SeriesCanClinch = true; int g_RoundNumber = -1; // The round number, 0-indexed. -1 if the match is not live. // The active map number, used by stats. Required as the calculated round number changes immediately // as a map ends, but before the map changes to the next. -int g_MapNumber = 0; +int g_MapNumber = 0; // the current map number, starting at 0. +int g_NumberOfMapsInSeries = 0; // the number of maps to play in the series. char g_MatchID[MATCH_ID_LENGTH]; ArrayList g_MapPoolList = null; ArrayList g_TeamAuths[MATCHTEAM_COUNT]; @@ -143,12 +148,19 @@ MatchSideType g_MatchSideType = MatchSideType_Standard; ArrayList g_CvarNames = null; ArrayList g_CvarValues = null; bool g_InScrimMode = false; + +/** Knife for sides **/ bool g_HasKnifeRoundStarted = false; +Get5Team g_KnifeWinnerTeam = Get5Team_None; +Handle g_KnifeChangedCvars = INVALID_HANDLE; +Handle g_KnifeDecisionTimer = INVALID_HANDLE; +Handle g_KnifeCountdownTimer = INVALID_HANDLE; /** Pausing **/ -bool g_IsChangingPauseState = false; // Used to prevent mp_pause_match and mp_unpause_match from being called directly. -Get5Team g_PausingTeam = Get5Team_None; // The team that last called for a pause. -Get5PauseType g_PauseType = Get5PauseType_None; // The type of pause last initiated. +bool g_IsChangingPauseState = + false; // Used to prevent mp_pause_match and mp_unpause_match from being called directly. +Get5Team g_PausingTeam = Get5Team_None; // The team that last called for a pause. +Get5PauseType g_PauseType = Get5PauseType_None; // The type of pause last initiated. int g_LatestPauseDuration = 0; bool g_TeamReadyForUnpause[MATCHTEAM_COUNT]; bool g_TeamGivenStopCommand[MATCHTEAM_COUNT]; @@ -166,23 +178,22 @@ Menu g_ActiveVetoMenu = null; /** Backup data **/ bool g_WaitingForRoundBackup = false; -bool g_SavedValveBackup = false; bool g_DoingBackupRestoreNow = false; // Stats values StringMap g_FlashbangContainer; // Stores flashbang-entity-id -> Get5FlashbangDetonatedEvent. -StringMap g_HEGrenadeContainer; // Stores flashbang-entity-id -> Get5HEDetonatedEvent. -StringMap g_MolotovContainer; // Stores flashbang-entity-id -> Get5MolotovDetonatedEvent. +StringMap g_HEGrenadeContainer; // Stores he-entity-id -> Get5HEDetonatedEvent. +StringMap g_MolotovContainer; // Stores molotov-entity-id -> Get5MolotovDetonatedEvent. int g_LatestUserIdToDetonateMolotov = 0; // Molotov detonate and start-burning/extinguish are two separate events always fired right // after each other. We need this to bind them together as detonate does not have client id. int g_LatestMolotovToExtinguishBySmoke = 0; // Attributes extinguish booleans to smoke grenades. +bool g_FirstKillDone = false; +bool g_FirstDeathDone = false; bool g_SetTeamClutching[4]; int g_RoundKills[MAXPLAYERS + 1]; // kills per round each client has gotten int g_RoundClutchingEnemyCount[MAXPLAYERS + 1]; // number of enemies left alive when last alive on your team -bool g_TeamFirstKillDone[MATCHTEAM_COUNT]; -bool g_TeamFirstDeathDone[MATCHTEAM_COUNT]; int g_PlayerKilledBy[MAXPLAYERS + 1]; float g_PlayerKilledByTime[MAXPLAYERS + 1]; int g_DamageDone[MAXPLAYERS + 1][MAXPLAYERS + 1]; @@ -204,38 +215,24 @@ bool g_ClientReady[MAXPLAYERS + 1]; // Whether clients are marked ready. int g_TeamSide[MATCHTEAM_COUNT]; // Current CS_TEAM_* side for the team. int g_TeamStartingSide[MATCHTEAM_COUNT]; int g_ReadyTimeWaitingUsed = 0; -char g_DefaultTeamColors[][] = { - TEAM1_COLOR, - TEAM2_COLOR, - "{NORMAL}", - "{NORMAL}", -}; char g_LastKickedPlayerAuth[64]; -bool g_ForceWinnerSignal = false; -Get5Team g_ForcedWinner = Get5Team_None; - /** Chat aliases loaded **/ #define ALIAS_LENGTH 64 #define COMMAND_LENGTH 64 ArrayList g_ChatAliases; ArrayList g_ChatAliasesCommands; -/** Map game-state **/ -Get5Team g_KnifeWinnerTeam = Get5Team_None; - /** Map-game state not related to the actual gameplay. **/ char g_DemoFileName[PLATFORM_MAX_PATH]; bool g_MapChangePending = false; -bool g_MovingClientToCoach[MAXPLAYERS + 1]; bool g_PendingSideSwap = false; // version check state bool g_RunningPrereleaseVersion = false; bool g_NewerVersionAvailable = false; -Handle g_KnifeChangedCvars = INVALID_HANDLE; Handle g_MatchConfigChangedCvars = INVALID_HANDLE; /** Forwards **/ @@ -291,6 +288,7 @@ Handle g_OnSidePicked = INVALID_HANDLE; #include "get5/natives.sp" #include "get5/pausing.sp" #include "get5/readysystem.sp" +#include "get5/recording.sp" #include "get5/stats.sp" #include "get5/teamlogic.sp" #include "get5/tests.sp" @@ -298,7 +296,7 @@ Handle g_OnSidePicked = INVALID_HANDLE; // clang-format off public Plugin myinfo = { name = "Get5", - author = "splewis", + author = "splewis, nickdnk & PhlexPlexico", description = "", version = PLUGIN_VERSION, url = "https://github.com/splewis/get5" @@ -313,8 +311,8 @@ public void OnPluginStart() { InitDebugLog(DEBUG_CVAR, "get5"); LogDebug("OnPluginStart version=%s", PLUGIN_VERSION); - // Because we use SDKHooks for damage, we need to re-hook clients that are already on the server in case - // the plugin is reloaded. This includes bots. + // Because we use SDKHooks for damage, we need to re-hook clients that are already on the server + // in case the plugin is reloaded. This includes bots. LOOP_CLIENTS(i) { if (IsValidClient(i)) { Stats_HookDamageForClient(i); @@ -354,8 +352,9 @@ public void OnPluginStart() { g_CheckAuthsCvar = CreateConVar("get5_check_auths", "1", "If set to 0, get5 will not force players to the correct team based on steamid"); - g_DemoNameFormatCvar = CreateConVar("get5_demo_name_format", "{MATCHID}_map{MAPNUMBER}_{MAPNAME}", - "Format for demo file names, use \"\" to disable"); + g_DemoNameFormatCvar = CreateConVar( + "get5_demo_name_format", "{TIME}_{MATCHID}_map{MAPNUMBER}_{MAPNAME}", + "Format for demo file names, use \"\" to disable. Do not remove the {TIME} placeholder if you use the backup system."); g_DisplayGotvVetoCvar = CreateConVar("get5_display_gotv_veto", "0", "Whether to wait for map vetos to be printed to GOTV before changing map"); @@ -374,8 +373,12 @@ public void OnPluginStart() { g_KickClientsWithNoMatchCvar = CreateConVar("get5_kick_when_no_match_loaded", "0", "Whether the plugin kicks new clients when no match is loaded"); - g_LiveCfgCvar = - CreateConVar("get5_live_cfg", "get5/live.cfg", "Config file to exec when the game goes live"); + g_LiveCfgCvar = CreateConVar("get5_live_cfg", "get5/live.cfg", + "Config file to exec when the game goes live."); + g_WarmupCfgCvar = + CreateConVar("get5_warmup_cfg", "get5/warmup.cfg", "Config file to exec in warmup periods."); + g_KnifeCfgCvar = + CreateConVar("get5_knife_cfg", "get5/knife.cfg", "Config file to exec in knife periods."); g_LiveCountdownTimeCvar = CreateConVar( "get5_live_countdown_time", "10", "Number of seconds used to count down when a match is going live", 0, true, 5.0, true, 60.0); @@ -421,7 +424,7 @@ public void OnPluginStart() { "get5_time_to_make_knife_decision", "60", "Time (in seconds) a team has to make a !stay/!swap decision after winning knife round, 0=unlimited"); g_TimeFormatCvar = CreateConVar( - "get5_time_format", "%Y-%m-%d_%H", + "get5_time_format", "%Y-%m-%d_%H-%M-%S", "Time format to use when creating file names. Don't tweak this unless you know what you're doing! Avoid using spaces or colons."); g_VetoConfirmationTimeCvar = CreateConVar( "get5_veto_confirmation_time", "2.0", @@ -429,9 +432,21 @@ public void OnPluginStart() { g_VetoCountdownCvar = CreateConVar("get5_veto_countdown", "5", "Seconds to countdown before veto process commences. Set to \"0\" to disable."); - g_WarmupCfgCvar = - CreateConVar("get5_warmup_cfg", "get5/warmup.cfg", "Config file to exec in warmup periods"); - g_PrintUpdateNoticeCvar = CreateConVar("get5_print_update_notice", "1", "Whether to print to chat when the game goes live if a new version of Get5 is available."); + g_PrintUpdateNoticeCvar = CreateConVar( + "get5_print_update_notice", "1", + "Whether to print to chat when the game goes live if a new version of Get5 is available."); + g_RoundBackupPathCvar = CreateConVar( + "get5_backup_path", "", + "The folder to save backup files in, relative to the csgo directory. If defined, it must not start with a slash and must end with a slash."); + g_PhaseAnnouncementCountCvar = CreateConVar( + "get5_phase_announcement_count", "5", + "The number of times Get5 will print 'Knife' or 'Match is LIVE' when the game starts. Set to 0 to disable."); + g_Team1NameColorCvar = CreateConVar("get5_team1_color", "{LIGHT_GREEN}", + "The color used for the name of team 1 in chat messages."); + g_Team2NameColorCvar = CreateConVar("get5_team2_color", "{PINK}", + "The color used for the name of team 2 in chat messages."); + g_SpecNameColorCvar = CreateConVar("get5_spec_color", "{NORMAL}", + "The color used for the name of spectators in chat messages."); /** Create and exec plugin's configuration file **/ AutoExecConfig(true, "get5"); @@ -445,6 +460,8 @@ public void OnPluginStart() { g_VersionCvar.SetString(PLUGIN_VERSION); g_CoachingEnabledCvar = FindConVar("sv_coaching_enabled"); + g_CoachingEnabledCvar.AddChangeHook( + CoachingChangedHook); // used to move people off coaching if it gets disabled. /** Client commands **/ g_ChatAliases = new ArrayList(ByteCountToCells(ALIAS_LENGTH)); @@ -526,10 +543,11 @@ public void OnPluginStart() { /** Hooks **/ HookEvent("cs_win_panel_match", Event_MatchOver); + HookEvent("cs_win_panel_round", Event_RoundWinPanel, EventHookMode_Pre); HookEvent("player_connect_full", Event_PlayerConnectFull); HookEvent("player_disconnect", Event_PlayerDisconnect); HookEvent("player_spawn", Event_PlayerSpawn); - HookEvent("round_end", Event_RoundEnd); + HookEvent("round_end", Event_RoundEnd, EventHookMode_Pre); HookEvent("round_freeze_end", Event_FreezeEnd); HookEvent("round_prestart", Event_RoundPreStart); HookEvent("round_start", Event_RoundStart); @@ -610,51 +628,57 @@ public void OnPluginStart() { CheckForLatestVersion(); } -public Action Timer_InfoMessages(Handle timer) { +static Action Timer_InfoMessages(Handle timer) { + if (g_GameState == Get5State_Live || g_GameState == Get5State_None) { + return Plugin_Continue; + } + + char readyCommandFormatted[64]; + FormatChatCommand(readyCommandFormatted, sizeof(readyCommandFormatted), "!ready"); + // Handle pre-veto messages if (g_GameState == Get5State_PreVeto) { if (IsTeamsReady() && !IsSpectatorsReady()) { Get5_MessageToAll("%t", "WaitingForCastersReadyInfoMessage", - g_FormattedTeamNames[Get5Team_Spec]); + g_FormattedTeamNames[Get5Team_Spec], readyCommandFormatted); } else { - Get5_MessageToAll("%t", "ReadyToVetoInfoMessage"); + Get5_MessageToAll("%t", "ReadyToVetoInfoMessage", readyCommandFormatted); } MissingPlayerInfoMessage(); - } - - // Handle warmup state, provided we're not waiting for a map change - if (g_GameState == Get5State_Warmup && !g_MapChangePending) { + } else if (g_GameState == Get5State_Warmup && !g_MapChangePending) { + // Handle warmup state, provided we're not waiting for a map change // Backups take priority if (!IsTeamsReady() && g_WaitingForRoundBackup) { - Get5_MessageToAll("%t", "ReadyToRestoreBackupInfoMessage"); + Get5_MessageToAll("%t", "ReadyToRestoreBackupInfoMessage", readyCommandFormatted); return Plugin_Continue; } // Find out what we're waiting for if (IsTeamsReady() && !IsSpectatorsReady()) { Get5_MessageToAll("%t", "WaitingForCastersReadyInfoMessage", - g_FormattedTeamNames[Get5Team_Spec]); + g_FormattedTeamNames[Get5Team_Spec], readyCommandFormatted); } else { if (g_MapSides.Get(Get5_GetMapNumber()) == SideChoice_KnifeRound) { - Get5_MessageToAll("%t", "ReadyToKnifeInfoMessage"); + Get5_MessageToAll("%t", "ReadyToKnifeInfoMessage", readyCommandFormatted); } else { - Get5_MessageToAll("%t", "ReadyToStartInfoMessage"); + Get5_MessageToAll("%t", "ReadyToStartInfoMessage", readyCommandFormatted); } } MissingPlayerInfoMessage(); } else if (g_DisplayGotvVetoCvar.BoolValue && g_GameState == Get5State_Warmup && - g_MapChangePending) { + g_MapChangePending && GetTvDelay() > 0) { Get5_MessageToAll("%t", "WaitingForGOTVVetoInfoMessage"); - } - - // Handle waiting for knife decision - if (g_GameState == Get5State_WaitingForKnifeRoundDecision) { + } else if (g_GameState == Get5State_WaitingForKnifeRoundDecision) { + // Handle waiting for knife decision + char formattedStayCommand[64]; + FormatChatCommand(formattedStayCommand, sizeof(formattedStayCommand), "!stay"); + char formattedSwapCommand[64]; + FormatChatCommand(formattedSwapCommand, sizeof(formattedSwapCommand), "!swap"); Get5_MessageToAll("%t", "WaitingForEnemySwapInfoMessage", - g_FormattedTeamNames[g_KnifeWinnerTeam]); - } - - // Handle postgame - if (g_GameState == Get5State_PostGame) { + g_FormattedTeamNames[g_KnifeWinnerTeam], formattedStayCommand, + formattedSwapCommand); + } else if (g_GameState == Get5State_PostGame && GetTvDelay() > 0) { + // Handle postgame Get5_MessageToAll("%t", "WaitingForGOTVBrodcastEndingInfoMessage"); } @@ -663,7 +687,6 @@ public Action Timer_InfoMessages(Handle timer) { public void OnClientAuthorized(int client, const char[] auth) { SetClientReady(client, false); - g_MovingClientToCoach[client] = false; if (StrEqual(auth, "BOT", false)) { return; } @@ -672,35 +695,31 @@ public void OnClientAuthorized(int client, const char[] auth) { Get5Team team = GetClientMatchTeam(client); if (team == Get5Team_None) { RememberAndKickClient(client, "%t", "YouAreNotAPlayerInfoMessage"); - } else { - int teamCount = CountPlayersOnMatchTeam(team, client); - if (teamCount >= g_PlayersPerTeam && !g_CoachingEnabledCvar.BoolValue) { - KickClient(client, "%t", "TeamIsFullInfoMessage"); - } + } else if (CountPlayersOnTeam(team, client) >= g_PlayersPerTeam && + (!g_CoachingEnabledCvar.BoolValue || + CountCoachesOnTeam(team, client) >= g_CoachesPerTeam)) { + KickClient(client, "%t", "TeamIsFullInfoMessage"); } } } -public void RememberAndKickClient(int client, const char[] format, const char[] translationPhrase) { +void RememberAndKickClient(int client, const char[] format, const char[] translationPhrase) { GetAuth(client, g_LastKickedPlayerAuth, sizeof(g_LastKickedPlayerAuth)); KickClient(client, format, translationPhrase); } public void OnClientPutInServer(int client) { - Stats_HookDamageForClient(client); + LogDebug("OnClientPutInServer"); + Stats_HookDamageForClient(client); // Also needed for bots! if (IsFakeClient(client)) { return; } - - CheckAutoLoadConfig(); - if (g_GameState <= Get5State_Warmup && g_GameState != Get5State_None) { - if (GetRealClientCount() <= 1) { - ExecCfg(g_WarmupCfgCvar); - EnsureIndefiniteWarmup(); - } - } - + // If a player joins during freezetime, ensure their round stats are 0, as there will be no + // round-start event to do it. Maybe this could just be freezetime end? Stats_ResetClientRoundValues(client); + // Because OnConfigsExecuted may run before a client is on the server, we have to repeat the + // start-logic here when the first client connects. + SetServerStateOnStartup(false); } public void OnClientPostAdminCheck(int client) { @@ -731,7 +750,7 @@ public void OnClientSayCommand_Post(int client, const char[] command, const char EventLogger_LogAndDeleteEvent(event); } } - CheckForChatAlias(client, command, sArgs); + CheckForChatAlias(client, sArgs); } /** @@ -740,7 +759,7 @@ public void OnClientSayCommand_Post(int client, const char[] command, const char * if a player does not select a team but leaves their mouse over one, they are * put on that team and spawned, so we can't allow that. */ -public Action Event_PlayerConnectFull(Event event, const char[] name, bool dontBroadcast) { +static Action Event_PlayerConnectFull(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if (IsValidClient(client)) { char ipAddress[32]; @@ -761,7 +780,7 @@ public Action Event_PlayerConnectFull(Event event, const char[] name, bool dontB } } -public Action Event_PlayerDisconnect(Event event, const char[] name, bool dontBroadcast) { +static Action Event_PlayerDisconnect(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if (client > 0) { @@ -782,50 +801,65 @@ public Action Event_PlayerDisconnect(Event event, const char[] name, bool dontBr g_GameState < Get5State_PostGame && GetRealClientCount() == 0 && !g_MapChangePending) { g_TeamSeriesScores[Get5Team_1] = 0; g_TeamSeriesScores[Get5Team_2] = 0; - EndSeries(); + StopRecording(); + EndSeries(Get5Team_None, false, 0.0, false); } } -public void OnMapStart() { +// This runs every time a map starts *or* when the plugin is reloaded. +public void OnConfigsExecuted() { + LogDebug("OnConfigsExecuted"); + // If the server has hibernation enabled, running this without a delay will cause it to frequently + // fail with "Gamerules lookup failed" probably due to some odd internal race-condition where the + // game is not yet running when we attempt to determine its "is paused" or "is in warmup" state. + // Putting it on a 1 second callback seems to solve this problem. + CreateTimer(1.0, Timer_ConfigsExecutedCallback); +} + +static Action Timer_ConfigsExecutedCallback(Handle timer) { + LogDebug("OnConfigsExecuted timer callback"); + g_MapChangePending = false; + g_DoingBackupRestoreNow = false; + g_ReadyTimeWaitingUsed = 0; + g_HasKnifeRoundStarted = false; + // Recording is always automatically stopped on map change, and + // since there are no hooks to detect tv_stoprecord, we reset + // our recording var if a map change is performed unexpectedly. + g_DemoFileName = ""; DeleteOldBackups(); + // Always reset ready status on map start ResetReadyStatus(); - LOOP_TEAMS(team) { - g_TeamGivenStopCommand[team] = false; - g_TeamReadyForUnpause[team] = false; - g_TacticalPauseTimeUsed[team] = 0; - g_TacticalPausesUsed[team] = 0; - g_ReadyTimeWaitingUsed = 0; - g_TechnicalPausesUsed[team] = 0; - } - if (g_WaitingForRoundBackup) { - ChangeState(Get5State_Warmup); - ExecCfg(g_LiveCfgCvar); - SetMatchTeamCvars(); - ExecuteMatchConfigCvars(); - EnsureIndefiniteWarmup(); + if (CheckAutoLoadConfig()) { + // If gamestate is none and a config was autoloaded, a match config will set all of the below + // state. + return; } -} - -public void OnConfigsExecuted() { - SetStartingTeams(); - CheckAutoLoadConfig(); - if (g_GameState == Get5State_PostGame) { - ChangeState(Get5State_Warmup); + LOOP_TEAMS(team) { + g_TeamGivenStopCommand[team] = false; + g_TeamReadyForUnpause[team] = false; + if (!g_WaitingForRoundBackup) { + g_TacticalPauseTimeUsed[team] = 0; + g_TacticalPausesUsed[team] = 0; + g_TechnicalPausesUsed[team] = 0; + } } - if (g_GameState == Get5State_Warmup || g_GameState == Get5State_Veto) { - ExecCfg(g_WarmupCfgCvar); - SetMatchTeamCvars(); - ExecuteMatchConfigCvars(); - EnsureIndefiniteWarmup(); + // On map start, always put the game in warmup mode. + // When executing a backup load, the live config is loaded and warmup ends after players ready-up + // again. + SetServerStateOnStartup(true); + // This must not be called when waiting for a backup, as it will set the sides incorrectly if the + // team swapped in knife or if the backup target is the second half. + if (!g_WaitingForRoundBackup) { + SetStartingTeams(); } } -public Action Timer_CheckReady(Handle timer) { +static Action Timer_CheckReady(Handle timer) { if (g_GameState == Get5State_None) { return Plugin_Continue; } @@ -845,7 +879,7 @@ public Action Timer_CheckReady(Handle timer) { // We don't wait for spectators when initiating veto LogDebug("Timer_CheckReady: starting veto"); ChangeState(Get5State_Veto); - ServerCommand("mp_restartgame 1"); + RestartGame(); CreateVeto(); } else { CheckReadyWaitingTimes(); @@ -872,6 +906,7 @@ public Action Timer_CheckReady(Handle timer) { LogDebug("Timer_CheckReady: starting without a knife round"); StartGame(false); } + StartRecording(); } else { CheckReadyWaitingTimes(); } @@ -887,21 +922,21 @@ static void CheckReadyWaitingTimes() { bool team1Forfeited = CheckReadyWaitingTime(Get5Team_1); bool team2Forfeited = CheckReadyWaitingTime(Get5Team_2); - if (team1Forfeited && team2Forfeited) { - g_ForcedWinner = Get5Team_None; - Stats_Forfeit(Get5Team_None); - } else if (team1Forfeited) { - g_ForcedWinner = Get5Team_2; - Stats_Forfeit(Get5Team_1); - } else if (team2Forfeited) { - g_ForcedWinner = Get5Team_1; - Stats_Forfeit(Get5Team_2); - } - if (team1Forfeited || team2Forfeited) { - g_ForceWinnerSignal = true; - ChangeState(Get5State_None); - EndSeries(); + Stats_Forfeit(); + float minDelay = 5.0; + StopRecording(minDelay); + float endDelay = float(GetTvDelay()); + if (endDelay < minDelay) { + endDelay = minDelay; + } + if (team1Forfeited && team2Forfeited) { + EndSeries(Get5Team_None, false, endDelay); + } else if (team1Forfeited) { + EndSeries(Get5Team_2, false, endDelay); + } else { + EndSeries(Get5Team_1, false, endDelay); + } } } } @@ -927,23 +962,47 @@ static bool CheckReadyWaitingTime(Get5Team team) { return false; } -static void CheckAutoLoadConfig() { - if (g_GameState == Get5State_None) { +bool CheckAutoLoadConfig() { + if (g_GameState == Get5State_None && !g_WaitingForRoundBackup) { char autoloadConfig[PLATFORM_MAX_PATH]; g_AutoLoadConfigCvar.GetString(autoloadConfig, sizeof(autoloadConfig)); if (!StrEqual(autoloadConfig, "")) { - LoadMatchConfig(autoloadConfig); + bool loaded = LoadMatchConfig(autoloadConfig); // return false if match config load fails! + if (loaded) { + LogMessage("Match configuration was loaded via get5_autoload_config."); + } + return loaded; } } + return false; } /** * Client and server commands. */ -public Action Command_EndMatch(int client, int args) { +static Action Command_EndMatch(int client, int args) { if (g_GameState == Get5State_None) { - return Plugin_Handled; + ReplyToCommand(client, "No match is configured; nothing to end."); + return; + } + + Get5Team winningTeam = Get5Team_None; // defaults to tie + if (args >= 1) { + char forcedWinningTeam[8]; + GetCmdArg(1, forcedWinningTeam, sizeof(forcedWinningTeam)); + if (StrEqual("team1", forcedWinningTeam, false)) { + winningTeam = Get5Team_1; + } else if (StrEqual("team2", forcedWinningTeam, false)) { + winningTeam = Get5Team_2; + } else { + ReplyToCommand(client, "Usage: get5_endmatch (omit team for tie)"); + return; + } + } + + if (IsPaused()) { + UnpauseGame(Get5Team_None); } // Call game-ending forwards. @@ -952,45 +1011,51 @@ public Action Command_EndMatch(int client, int args) { int team2score = CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_2)); Get5MapResultEvent mapResultEvent = new Get5MapResultEvent( - g_MatchID, g_MapNumber, new Get5Winner(Get5Team_None, Get5Side_None), team1score, team2score); + g_MatchID, g_MapNumber, + new Get5Winner(winningTeam, view_as(Get5TeamToCSTeam(winningTeam))), team1score, + team2score); LogDebug("Calling Get5_OnMapResult()"); - Call_StartForward(g_OnMapResult); Call_PushCell(mapResultEvent); Call_Finish(); - EventLogger_LogAndDeleteEvent(mapResultEvent); - Get5SeriesResultEvent resultEvent = - new Get5SeriesResultEvent(g_MatchID, new Get5Winner(Get5Team_None, Get5Side_None), - g_TeamSeriesScores[Get5Team_1], g_TeamSeriesScores[Get5Team_2]); - - LogDebug("Calling Get5_OnSeriesResult()"); - Call_StartForward(g_OnSeriesResult); - Call_PushCell(resultEvent); - Call_Finish(); + StopRecording(1.0); // must go before EndSeries as it depends on g_MatchID. - EventLogger_LogAndDeleteEvent(resultEvent); + // No delay required when not kicking players. + EndSeries(winningTeam, false, 0.0, false); - ChangeState(Get5State_None); UpdateClanTags(); - Get5_MessageToAll("%t", "AdminForceEndInfoMessage"); - RestoreCvars(g_MatchConfigChangedCvars); - StopRecording(); + if (winningTeam == Get5Team_None) { + Get5_MessageToAll("%t", "AdminForceEndInfoMessage"); + } else { + Get5_MessageToAll("%t", "AdminForceEndWithWinnerInfoMessage", + g_FormattedTeamNames[winningTeam]); + } if (g_ActiveVetoMenu != null) { g_ActiveVetoMenu.Cancel(); } - return Plugin_Handled; + if (g_KnifeCountdownTimer != INVALID_HANDLE) { + LogDebug("Killing knife announce countdown timer."); + delete g_KnifeCountdownTimer; + } + + if (g_KnifeDecisionTimer != INVALID_HANDLE) { + LogDebug("Killing knife decision timer."); + delete g_KnifeDecisionTimer; + } + + RestartGame(); } -public Action Command_LoadMatch(int client, int args) { +static Action Command_LoadMatch(int client, int args) { if (g_GameState != Get5State_None) { - ReplyToCommand(client, "Cannot load a match when a match is already loaded"); - return Plugin_Handled; + ReplyToCommand(client, "Cannot load a match config when another is already loaded."); + return; } char arg[PLATFORM_MAX_PATH]; @@ -1001,14 +1066,12 @@ public Action Command_LoadMatch(int client, int args) { } else { ReplyToCommand(client, "Usage: get5_loadmatch "); } - - return Plugin_Handled; } -public Action Command_LoadMatchUrl(int client, int args) { +static Action Command_LoadMatchUrl(int client, int args) { if (g_GameState != Get5State_None) { - ReplyToCommand(client, "Cannot load a match config with another match already loaded"); - return Plugin_Handled; + ReplyToCommand(client, "Cannot load a match config when another is already loaded."); + return; } bool steamWorksAvaliable = LibraryExists("SteamWorks"); @@ -1027,14 +1090,12 @@ public Action Command_LoadMatchUrl(int client, int args) { ReplyToCommand(client, "Usage: get5_loadmatch_url "); } } - - return Plugin_Handled; } -public Action Command_DumpStats(int client, int args) { +static Action Command_DumpStats(int client, int args) { if (g_GameState == Get5State_None) { - ReplyToCommand(client, "Cannot dump match stats with no match existing"); - return Plugin_Handled; + ReplyToCommand(client, "Cannot dump match stats when no match is loaded."); + return; } char arg[PLATFORM_MAX_PATH]; @@ -1050,11 +1111,9 @@ public Action Command_DumpStats(int client, int args) { } else { ReplyToCommand(client, "Failed to save match stats to %s", arg); } - - return Plugin_Handled; } -public Action Command_Stop(int client, int args) { +static Action Command_Stop(int client, int args) { if (!g_StopCommandEnabledCvar.BoolValue) { Get5_MessageToAll("%t", "StopCommandNotEnabled"); return Plugin_Handled; @@ -1078,12 +1137,14 @@ public Action Command_Stop(int client, int args) { Get5Team team = GetClientMatchTeam(client); g_TeamGivenStopCommand[team] = true; + char stopCommandFormatted[64]; + FormatChatCommand(stopCommandFormatted, sizeof(stopCommandFormatted), "!stop"); if (g_TeamGivenStopCommand[Get5Team_1] && !g_TeamGivenStopCommand[Get5Team_2]) { - Get5_MessageToAll("%t", "TeamWantsToReloadLastRoundInfoMessage", - g_FormattedTeamNames[Get5Team_1], g_FormattedTeamNames[Get5Team_2]); + Get5_MessageToAll("%t", "TeamWantsToReloadCurrentRound", g_FormattedTeamNames[Get5Team_1], + g_FormattedTeamNames[Get5Team_2], stopCommandFormatted); } else if (!g_TeamGivenStopCommand[Get5Team_1] && g_TeamGivenStopCommand[Get5Team_2]) { - Get5_MessageToAll("%t", "TeamWantsToReloadLastRoundInfoMessage", - g_FormattedTeamNames[Get5Team_2], g_FormattedTeamNames[Get5Team_1]); + Get5_MessageToAll("%t", "TeamWantsToReloadCurrentRound", g_FormattedTeamNames[Get5Team_2], + g_FormattedTeamNames[Get5Team_1], stopCommandFormatted); } else if (g_TeamGivenStopCommand[Get5Team_1] && g_TeamGivenStopCommand[Get5Team_2]) { RestoreLastRound(client); } @@ -1091,7 +1152,7 @@ public Action Command_Stop(int client, int args) { return Plugin_Handled; } -public void RestoreLastRound(int client) { +void RestoreLastRound(int client) { LOOP_TEAMS(x) { g_TeamGivenStopCommand[x] = false; } @@ -1099,7 +1160,7 @@ public void RestoreLastRound(int client) { char lastBackup[PLATFORM_MAX_PATH]; g_LastGet5BackupCvar.GetString(lastBackup, sizeof(lastBackup)); if (!StrEqual(lastBackup, "")) { - if (RestoreFromBackup(lastBackup)) { + if (RestoreFromBackup(lastBackup, false)) { Get5_MessageToAll("%t", "BackupLoadedInfoMessage", lastBackup); // Fix the last backup cvar since it gets reset. g_LastGet5BackupCvar.SetString(lastBackup); @@ -1115,22 +1176,42 @@ public void RestoreLastRound(int client) { * Game Events *not* related to the stats tracking system. */ -public Action Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) { +static Action Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) { if (g_GameState != Get5State_None && g_GameState < Get5State_KnifeRound) { int client = GetClientOfUserId(event.GetInt("userid")); CreateTimer(0.1, Timer_ReplenishMoney, client, TIMER_FLAG_NO_MAPCHANGE); } } -public Action Timer_ReplenishMoney(Handle timer, int client) { +static Action Timer_ReplenishMoney(Handle timer, int client) { if (IsPlayer(client) && OnActiveTeam(client)) { SetEntProp(client, Prop_Send, "m_iAccount", GetCvarIntSafe("mp_maxmoney")); } } -public Action Event_MatchOver(Event event, const char[] name, bool dontBroadcast) { +static Action Event_MatchOver(Event event, const char[] name, bool dontBroadcast) { LogDebug("Event_MatchOver"); + if (g_GameState == Get5State_None) { + return; + } + + // This ensures that the mp_match_restart_delay is not shorter + // than what is required for the GOTV recording to finish. + float restartDelay = GetCurrentMatchRestartDelay(); + float requiredDelay = float(GetTvDelay() + MATCH_END_DELAY_AFTER_TV); + if (requiredDelay > restartDelay) { + LogDebug("Extended mp_match_restart_delay from %f to %f to ensure GOTV broadcast can finish.", + restartDelay, requiredDelay); + SetCurrentMatchRestartDelay(requiredDelay); + restartDelay = requiredDelay; // reassigned because we reuse the variable below. + } + StopRecording(float(MATCH_END_DELAY_AFTER_TV)); + if (g_GameState == Get5State_Live) { + // If someone called for a pause in the last round; cancel it. + if (IsPaused()) { + UnpauseGame(Get5Team_None); + } // Figure out who won int t1score = CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_1)); int t2score = CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_2)); @@ -1142,26 +1223,20 @@ public Action Event_MatchOver(Event event, const char[] name, bool dontBroadcast } // If the round ends because the match is over, we clear the grenade container immediately as - // there will be no RoundStart event to do it, and the sideSwap check in RoundEnd will not - // trigger it either. + // they will not fire on their own if the game state is not live. Stats_ResetGrenadeContainers(); - // Write backup before series score increments - WriteBackup(); - // Update series scores Stats_UpdateMapScore(winningTeam); - AddMapScore(); g_TeamSeriesScores[winningTeam]++; - // Handle map end - int team1score = CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_1)); - int team2score = CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_2)); + g_TeamScoresPerMap.Set(g_MapNumber, t1score, view_as(Get5Team_1)); + g_TeamScoresPerMap.Set(g_MapNumber, t2score, view_as(Get5Team_2)); Get5MapResultEvent mapResultEvent = new Get5MapResultEvent( g_MatchID, g_MapNumber, - new Get5Winner(winningTeam, view_as(Get5TeamToCSTeam(winningTeam))), team1score, - team2score); + new Get5Winner(winningTeam, view_as(Get5TeamToCSTeam(winningTeam))), t1score, + t2score); LogDebug("Calling Get5_OnMapResult()"); @@ -1174,143 +1249,157 @@ public Action Event_MatchOver(Event event, const char[] name, bool dontBroadcast int t1maps = g_TeamSeriesScores[Get5Team_1]; int t2maps = g_TeamSeriesScores[Get5Team_2]; int tiedMaps = g_TeamSeriesScores[Get5Team_None]; + int remainingMaps = g_MapsToPlay.Length - t1maps - t2maps - tiedMaps; + + if (t1maps == t2maps) { + // As long as team scores are equal, we play until there are no maps left, regardless of + // clinch config. + if (remainingMaps <= 0) { + EndSeries(Get5Team_None, true, restartDelay); + return; + } + } else if (g_SeriesCanClinch) { + // This adjusts for ties! + int actualMapsToWin = MapsToWin(g_MapsToPlay.Length - tiedMaps); + if (t1maps == actualMapsToWin) { + // Team 1 won + EndSeries(Get5Team_1, true, restartDelay); + return; + } else if (t2maps == actualMapsToWin) { + // Team 2 won + EndSeries(Get5Team_2, true, restartDelay); + return; + } + } else if (remainingMaps <= 0) { + EndSeries(t1maps > t2maps ? Get5Team_1 : Get5Team_2, true, + restartDelay); // Tie handled in first if-block + return; + } - float minDelay = float(GetTvDelay()) + MATCH_END_DELAY_AFTER_TV; - - if (t1maps == g_MapsToWin) { - // Team 1 won - SeriesEndMessage(Get5Team_1); - DelayFunction(minDelay, EndSeries); - - } else if (t2maps == g_MapsToWin) { - // Team 2 won - SeriesEndMessage(Get5Team_2); - DelayFunction(minDelay, EndSeries); - - } else if (t1maps == t2maps && t1maps + tiedMaps == g_MapsToWin) { - // The whole series was a tie - SeriesEndMessage(Get5Team_None); - DelayFunction(minDelay, EndSeries); + if (t1maps > t2maps) { + Get5_MessageToAll("%t", "TeamWinningSeriesInfoMessage", g_FormattedTeamNames[Get5Team_1], + t1maps, t2maps); - } else if (g_BO2Match && Get5_GetMapNumber() == 2) { - // It was a bo2, and none of the teams got to 2 - SeriesEndMessage(Get5Team_None); - DelayFunction(minDelay, EndSeries); + } else if (t2maps > t1maps) { + Get5_MessageToAll("%t", "TeamWinningSeriesInfoMessage", g_FormattedTeamNames[Get5Team_2], + t2maps, t1maps); } else { - if (t1maps > t2maps) { - Get5_MessageToAll("%t", "TeamWinningSeriesInfoMessage", g_FormattedTeamNames[Get5Team_1], - t1maps, t2maps); - - } else if (t2maps > t1maps) { - Get5_MessageToAll("%t", "TeamWinningSeriesInfoMessage", g_FormattedTeamNames[Get5Team_2], - t2maps, t1maps); + Get5_MessageToAll("%t", "SeriesTiedInfoMessage", t1maps, t2maps); + } - } else { - Get5_MessageToAll("%t", "SeriesTiedInfoMessage", t1maps, t2maps); - } + char nextMap[PLATFORM_MAX_PATH]; + g_MapsToPlay.GetString(Get5_GetMapNumber(), nextMap, sizeof(nextMap)); - int index = Get5_GetMapNumber(); - char nextMap[PLATFORM_MAX_PATH]; - g_MapsToPlay.GetString(index, nextMap, sizeof(nextMap)); + char timeToMapChangeFormatted[8]; + convertSecondsToMinutesAndSeconds(RoundToFloor(restartDelay), timeToMapChangeFormatted, + sizeof(timeToMapChangeFormatted)); - g_MapChangePending = true; - Get5_MessageToAll("%t", "NextSeriesMapInfoMessage", nextMap); - ChangeState(Get5State_PostGame); - CreateTimer(minDelay, Timer_NextMatchMap); - } + g_MapChangePending = true; + FormatMapName(nextMap, nextMap, sizeof(nextMap), true, true); + Get5_MessageToAll("%t", "NextSeriesMapInfoMessage", nextMap, timeToMapChangeFormatted); + ChangeState(Get5State_PostGame); + // Subtracting 4 seconds makes the map change 1 second before the timer expires, as there is a 3 + // second built-in delay in the ChangeMap function called by Timer_NextMatchMap. + CreateTimer(restartDelay - 4, Timer_NextMatchMap); } +} - return Plugin_Continue; +Action Timer_NextMatchMap(Handle timer) { + char map[PLATFORM_MAX_PATH]; + g_MapsToPlay.GetString(Get5_GetMapNumber(), map, sizeof(map)); + // If you change these 3 seconds for whatever reason, you must adjust the counter-offset in + // Event_MatchOver. + ChangeMap(map, 3.0); } -static void SeriesEndMessage(Get5Team team) { - if (g_MapsToWin == 1) { - if (team == Get5Team_None) { +static void EndSeries(Get5Team winningTeam, bool printWinnerMessage, float restoreDelay, + bool kickPlayers = true) { + Stats_SeriesEnd(winningTeam); + + if (printWinnerMessage) { + if (winningTeam == Get5Team_None) { Get5_MessageToAll("%t", "TeamTiedMatchInfoMessage", g_FormattedTeamNames[Get5Team_1], g_FormattedTeamNames[Get5Team_2]); } else { - Get5_MessageToAll("%t", "TeamWonMatchInfoMessage", g_FormattedTeamNames[team]); + if (g_MapsToPlay.Length == 1) { + Get5_MessageToAll("%t", "TeamWonMatchInfoMessage", g_FormattedTeamNames[winningTeam]); + } else { + Get5_MessageToAll("%t", "TeamWonSeriesInfoMessage", g_FormattedTeamNames[winningTeam], + g_TeamSeriesScores[winningTeam], + g_TeamSeriesScores[OtherMatchTeam(winningTeam)]); + } } - } else { - if (team == Get5Team_None) { - // BO2 split. - Get5_MessageToAll("%t", "TeamsSplitSeriesBO2InfoMessage", g_FormattedTeamNames[Get5Team_1], - g_FormattedTeamNames[Get5Team_2]); + } + + Get5SeriesResultEvent event = new Get5SeriesResultEvent( + g_MatchID, new Get5Winner(winningTeam, view_as(Get5TeamToCSTeam(winningTeam))), + g_TeamSeriesScores[Get5Team_1], g_TeamSeriesScores[Get5Team_2]); + + LogDebug("Calling Get5_OnSeriesResult()"); + + Call_StartForward(g_OnSeriesResult); + Call_PushCell(event); + Call_Finish(); + + EventLogger_LogAndDeleteEvent(event); + ChangeState(Get5State_None); + g_MatchID = ""; + // We don't want to kick players until after the specified delay, as it will kick casters + // potentially before GOTV ends. + if (kickPlayers && g_KickClientsWithNoMatchCvar.BoolValue) { + if (restoreDelay < 0.1) { + KickPlayers(); } else { - Get5_MessageToAll("%t", "TeamWonSeriesInfoMessage", g_FormattedTeamNames[team], - g_TeamSeriesScores[team], g_TeamSeriesScores[OtherMatchTeam(team)]); + CreateTimer(restoreDelay, Timer_KickOnEnd, _, TIMER_FLAG_NO_MAPCHANGE); } } -} - -public Action Timer_NextMatchMap(Handle timer) { - if (g_GameState >= Get5State_Live) - StopRecording(); - int index = Get5_GetMapNumber(); - char map[PLATFORM_MAX_PATH]; - g_MapsToPlay.GetString(index, map, sizeof(map)); - - if (!g_SkipVeto && g_DisplayGotvVetoCvar.BoolValue && index == 0) { - float minDelay = float(GetTvDelay()) + MATCH_END_DELAY_AFTER_TV; - ChangeMap(map, minDelay); + if (restoreDelay < 0.1) { + // When force-ending the match there is no delay. + RestoreCvars(g_MatchConfigChangedCvars); } else { - ChangeMap(map); + // If we restore cvars immediately, it might change the tv_ params or set the + // mp_match_restart_delay to something lower, which is noticed by the game and may trigger a map + // change before GOTV broadcast ends, so we don't do this until the current match restart delay + // has passed. + CreateTimer(restoreDelay, Timer_RestoreMatchCvars, _, TIMER_FLAG_NO_MAPCHANGE); } } -public void KickClientsOnEnd() { - if (g_KickClientsWithNoMatchCvar.BoolValue) { - for (int i = 1; i <= MaxClients; i++) { - if (IsPlayer(i) && !(g_KickClientImmunityCvar.BoolValue && - CheckCommandAccess(i, "get5_kickcheck", ADMFLAG_CHANGEMAP))) { - KickClient(i, "%t", "MatchFinishedInfoMessage"); - } - } +static Action Timer_KickOnEnd(Handle timer) { + if (g_GameState == Get5State_None) { + // If a match was started before this event is triggered, don't do anything. + KickPlayers(); } + return Plugin_Handled; } -public void EndSeries() { - DelayFunction(10.0, KickClientsOnEnd); - StopRecording(); - - // Figure out who won - int t1maps = g_TeamSeriesScores[Get5Team_1]; - int t2maps = g_TeamSeriesScores[Get5Team_2]; - - Get5Team winningTeam = Get5Team_None; - if (t1maps > t2maps) { - winningTeam = Get5Team_1; - } else if (t2maps > t1maps) { - winningTeam = Get5Team_2; +static void KickPlayers() { + bool kickImmunity = g_KickClientImmunityCvar.BoolValue; + LOOP_CLIENTS(i) { + if (IsPlayer(i) && + !(kickImmunity && CheckCommandAccess(i, "get5_kickcheck", ADMFLAG_CHANGEMAP))) { + KickClient(i, "%t", "MatchFinishedInfoMessage"); + } } +} - if (g_ForceWinnerSignal) { - winningTeam = g_ForcedWinner; +static Action Timer_RestoreMatchCvars(Handle timer) { + if (g_GameState == Get5State_None) { + // Only reset if no game is running, otherwise a game started before the restart delay for + // another ends will mess this up. + RestoreCvars(g_MatchConfigChangedCvars); } - - Stats_SeriesEnd(winningTeam); - - Get5SeriesResultEvent event = new Get5SeriesResultEvent( - g_MatchID, new Get5Winner(winningTeam, view_as(Get5TeamToCSTeam(winningTeam))), - t1maps, t2maps); - - LogDebug("Calling Get5_OnSeriesResult()"); - - Call_StartForward(g_OnSeriesResult); - Call_PushCell(event); - Call_Finish(); - - EventLogger_LogAndDeleteEvent(event); - - RestoreCvars(g_MatchConfigChangedCvars); - ChangeState(Get5State_None); + return Plugin_Handled; } -public Action Event_RoundPreStart(Event event, const char[] name, bool dontBroadcast) { +static Action Event_RoundPreStart(Event event, const char[] name, bool dontBroadcast) { LogDebug("Event_RoundPreStart"); + if (g_GameState == Get5State_None) { + return; + } if (g_GameState == Get5State_Live) { // End lingering grenade trackers from previous round. @@ -1318,111 +1407,115 @@ public Action Event_RoundPreStart(Event event, const char[] name, bool dontBroad } if (g_PendingSideSwap) { - g_PendingSideSwap = false; SwapSides(); } - - if (g_GameState == Get5State_GoingLive) { - ChangeState(Get5State_Live); - } + g_PendingSideSwap = false; Stats_ResetRoundValues(); - if (g_GameState >= Get5State_Warmup && !g_DoingBackupRestoreNow) { - WriteBackup(); - } + // We need this for events that fire after the map ends, such as grenades detonating (or someone + // dying in fire), to be correct. It's sort of an edge-case, but due to how Get5_GetMapNumber + // works, it will return +1 if called after a map has been decided, but before the game actually + // stops, which could lead to events having the wrong map number, so we set both of these here and + // not in round_end + g_MapNumber = Get5_GetMapNumber(); + // Round number always -1 if not live. + g_RoundNumber = g_GameState != Get5State_Live ? -1 : GetRoundsPlayed(); } -public Action Event_FreezeEnd(Event event, const char[] name, bool dontBroadcast) { +static Action Event_FreezeEnd(Event event, const char[] name, bool dontBroadcast) { LogDebug("Event_FreezeEnd"); - // If someone changes the map while in a pause, we have to make sure we reset this state, as the UnpauseGame function - // will not be called to do it. FreezeTimeEnd is always called when the map initially loads. + // If someone changes the map while in a pause, we have to make sure we reset this state, as the + // UnpauseGame function will not be called to do it. FreezeTimeEnd is always called when the map + // initially loads. g_LatestPauseDuration = 0; g_PauseType = Get5PauseType_None; g_PausingTeam = Get5Team_None; // We always want this to be correct, regardless of game state. g_RoundStartedTime = GetEngineTime(); - if (g_GameState == Get5State_Live) { + if (g_GameState == Get5State_Live && !IsDoingRestoreOrMapChange()) { Stats_RoundStart(); } } -public void WriteBackup() { - if (!g_BackupSystemEnabledCvar.BoolValue) { - return; - } - - char path[PLATFORM_MAX_PATH]; - if (g_GameState == Get5State_Live) { - Format(path, sizeof(path), "get5_backup_match%s_map%d_round%d.cfg", g_MatchID, - GetMapStatsNumber(), GetRoundsPlayed()); - } else { - Format(path, sizeof(path), "get5_backup_match%s_map%d_prelive.cfg", g_MatchID, - GetMapStatsNumber()); - } - - LogDebug("created path %s", path); - - if (!g_DoingBackupRestoreNow) { - LogDebug("writing to %s", path); - WriteBackStructure(path); - g_LastGet5BackupCvar.SetString(path); - } -} - -public Action Event_RoundStart(Event event, const char[] name, bool dontBroadcast) { +static Action Event_RoundStart(Event event, const char[] name, bool dontBroadcast) { LogDebug("Event_RoundStart"); - // We need this for events that fire after the map ends, such as grenades detonating (or someone - // dying in fire), to be correct. It's sort of an edge-case, but due to how Get5_GetMapNumber - // works, it will return +1 if called after a map has been decided, but before the game actually - // stops, which could lead to events having the wrong map number. - g_MapNumber = Get5_GetMapNumber(); - // Always reset these on round start, regardless of game state. // This ensures that the functions that rely on these don't get messed up. g_RoundStartedTime = 0.0; g_BombPlantedTime = 0.0; g_BombSiteLastPlanted = Get5BombSite_Unknown; - if (g_GameState != Get5State_Live) { - g_RoundNumber = -1; // Round number always -1 if not yet live. + if (g_GameState == Get5State_None || IsDoingRestoreOrMapChange()) { + // Get5_OnRoundStart() is fired from within the backup event when loading the valve backup. return; } - // The same logic for tracking after-round-end events apply to g_RoundNumber, so that's set here - // as well. - g_RoundNumber = GetRoundsPlayed(); + // We cannot do this during warmup, as sending users into warmup post-knife triggers a round start + // event. We add an extra restart to clear lingering state from the knife round, such as the round + // indicator in the middle of the scoreboard not being reset. This also tightly couples the + // live-announcement to the actual live start. + if (!InWarmup()) { + if (g_GameState == Get5State_WaitingForKnifeRoundDecision) { + // Ensures that round end after knife sends players directly into warmup. + // This immediately triggers another Event_RoundStart, so we can return here and avoid + // writing backup twice. + LogDebug("Changed to warmup post knife."); + ExecCfg(g_WarmupCfgCvar); + StartWarmup(); + return; + } + if (g_GameState == Get5State_GoingLive) { + LogDebug("Changed to live."); + ChangeState(Get5State_Live); + RestartGame(); + CreateTimer(3.0, Timer_MatchLive, _, TIMER_FLAG_NO_MAPCHANGE); + return; // Next round start will take care of below, such as writing backup. + } + } + + // Ensures that players who connect during halftime/team swap are placed in their correct slots as + // soon as the following round starts. Otherwise they could be left on the "no team" screen and + // potentially ghost, depending on where the camera drops them. Especially important for coaches. + // We do this step *before* we write the backup, so we don't have any lingering players in case of + // a restore. + LOOP_CLIENTS(i) { + if (IsPlayer(i) && GetClientTeam(i) == CS_TEAM_NONE) { + CheckClientTeam(i); + } + } + + WriteBackup(); + + if (g_GameState != Get5State_Live) { + return; + } Get5RoundStartedEvent startEvent = new Get5RoundStartedEvent(g_MatchID, g_MapNumber, g_RoundNumber); - LogDebug("Calling Get5_OnRoundStart()"); - Call_StartForward(g_OnRoundStart); Call_PushCell(startEvent); Call_Finish(); - EventLogger_LogAndDeleteEvent(startEvent); } -public Action Event_RoundEnd(Event event, const char[] name, bool dontBroadcast) { - LogDebug("Event_RoundEnd"); - if (g_DoingBackupRestoreNow) { - return; - } - +static Action Event_RoundWinPanel(Event event, const char[] name, bool dontBroadcast) { + LogDebug("Event_RoundWinPanel"); if (g_GameState == Get5State_KnifeRound && g_HasKnifeRoundStarted) { g_HasKnifeRoundStarted = false; ChangeState(Get5State_WaitingForKnifeRoundDecision); - CreateTimer(1.0, Timer_PostKnife); + if (g_KnifeChangedCvars != INVALID_HANDLE) { + RestoreCvars(g_KnifeChangedCvars, true); + } int ctAlive = CountAlivePlayersOnTeam(CS_TEAM_CT); int tAlive = CountAlivePlayersOnTeam(CS_TEAM_T); - int winningCSTeam = CS_TEAM_NONE; + int winningCSTeam; if (ctAlive > tAlive) { winningCSTeam = CS_TEAM_CT; } else if (tAlive > ctAlive) { @@ -1435,31 +1528,83 @@ public Action Event_RoundEnd(Event event, const char[] name, bool dontBroadcast) } else if (tHealth > ctHealth) { winningCSTeam = CS_TEAM_T; } else { - if (GetRandomFloat(0.0, 1.0) < 0.5) { - winningCSTeam = CS_TEAM_CT; - } else { - winningCSTeam = CS_TEAM_T; - } + winningCSTeam = GetRandomFloat(0.0, 1.0) < 0.5 ? CS_TEAM_CT : CS_TEAM_T; + LogDebug("Randomized knife winner to side %d", winningCSTeam); } } g_KnifeWinnerTeam = CSTeamToGet5Team(winningCSTeam); + char formattedStayCommand[64]; + FormatChatCommand(formattedStayCommand, sizeof(formattedStayCommand), "!stay"); + char formattedSwapCommand[64]; + FormatChatCommand(formattedSwapCommand, sizeof(formattedSwapCommand), "!swap"); Get5_MessageToAll("%t", "WaitingForEnemySwapInfoMessage", - g_FormattedTeamNames[g_KnifeWinnerTeam]); + g_FormattedTeamNames[g_KnifeWinnerTeam], formattedStayCommand, + formattedSwapCommand); + + if (g_TeamTimeToKnifeDecisionCvar.FloatValue > 0) { + g_KnifeDecisionTimer = + CreateTimer(g_TeamTimeToKnifeDecisionCvar.FloatValue, Timer_ForceKnifeDecision); + } - if (g_TeamTimeToKnifeDecisionCvar.FloatValue > 0) - CreateTimer(g_TeamTimeToKnifeDecisionCvar.FloatValue, Timer_ForceKnifeDecision); + // This ensures that the correct graphic is displayed in-game for the winning team, as CTs will + // always win if the clock runs out. It also ensures that the fun fact displayed is correct; + // overriding to number of players killed by knife and no "CT won by running down the clock". + // MVP can still be on the losing team though. ran down". + int maxFrags = 0; + int topFragClient = 0; + int frags; + LOOP_CLIENTS(i) { + if (IsValidClient(i)) { + frags = GetClientFrags(i); + if (frags >= maxFrags) { + maxFrags = frags; + topFragClient = i; + } + } + } + if (topFragClient > 0) { + // Found here: + // https://github.com/SteamDatabase/GameTracking-CSGO/blob/master/csgo/bin/server_client_strings.txt + event.SetString("funfact_token", "#funfact_knife_kills"); + event.SetInt("funfact_player", topFragClient); + event.SetInt("funfact_data1", maxFrags); + } + event.SetInt("final_event", ConvertCSTeamToDefaultWinReason(winningCSTeam)); + } +} + +static Action Event_RoundEnd(Event event, const char[] name, bool dontBroadcast) { + LogDebug("Event_RoundEnd"); + if (g_GameState == Get5State_None || IsDoingRestoreOrMapChange()) { + return; + } + + if (g_GameState == Get5State_WaitingForKnifeRoundDecision && g_KnifeWinnerTeam != Get5Team_None) { + int winningCSTeam = Get5TeamToCSTeam(g_KnifeWinnerTeam); + // Event_RoundWinPanel is called before Event_RoundEnd, so that event handles knife winner. + // We override this event only to have the correct audio callout in the game. + event.SetInt("winner", winningCSTeam); + event.SetInt("reason", ConvertCSTeamToDefaultWinReason(winningCSTeam)); + return; } if (g_GameState == Get5State_Live) { int csTeamWinner = event.GetInt("winner"); - Get5_MessageToAll("%t", "CurrentScoreInfoMessage", g_TeamNames[Get5Team_1], + Get5_MessageToAll("%s {GREEN}%d {NORMAL}- {GREEN}%d %s", g_FormattedTeamNames[Get5Team_1], CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_1)), - CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_2)), g_TeamNames[Get5Team_2]); + CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_2)), + g_FormattedTeamNames[Get5Team_2]); Stats_RoundEnd(csTeamWinner); + if (g_DamagePrintCvar.BoolValue) { + LOOP_CLIENTS(i) { + PrintDamageInfo(i); // Checks valid client etc. on its own. + } + } + Get5RoundStatsUpdatedEvent statsEvent = new Get5RoundStatsUpdatedEvent(g_MatchID, g_MapNumber, g_RoundNumber); @@ -1499,12 +1644,6 @@ public Action Event_RoundEnd(Event event, const char[] name, bool dontBroadcast) } } - if (g_PendingSideSwap) { - // Normally we would do this in RoundStart, but since there is a significant delay between - // round *actual end* and and RoundStart when swapping sides, we do it here instead. - Stats_ResetGrenadeContainers(); - } - // CSRoundEndReason is incorrect in CSGO compared to the enumerations defined here: // https://github.com/alliedmodders/sourcemod/blob/master/plugins/include/cstrike.inc#L53-L77 // - which is why we subtract one. @@ -1522,13 +1661,21 @@ public Action Event_RoundEnd(Event event, const char[] name, bool dontBroadcast) Call_Finish(); EventLogger_LogAndDeleteEvent(roundEndEvent); + + // Reset this when a round ends, as voting has no reference to which round the teams wanted to + // restore to, so votes to restore during one round should not carry over into the next round, + // as it would just restore that round instead. + LOOP_TEAMS(t) { + if (g_TeamGivenStopCommand[t]) { + Get5_MessageToAll("%t", "StopCommandVotingReset", g_FormattedTeamNames[t]); + } + g_TeamGivenStopCommand[t] = false; + } } } -public void SwapSides() { +static void SwapSides() { LogDebug("SwapSides"); - // EventLogger_SideSwap(g_TeamSide[Get5Team_1], g_TeamSide[Get5Team_2]); - int tmp = g_TeamSide[Get5Team_1]; g_TeamSide[Get5Team_1] = g_TeamSide[Get5Team_2]; g_TeamSide[Get5Team_2] = tmp; @@ -1544,7 +1691,7 @@ public void SwapSides() { /** * Silences cvar changes when executing live/knife/warmup configs, *unless* it's sv_cheats. */ -public Action Event_CvarChanged(Event event, const char[] name, bool dontBroadcast) { +static Action Event_CvarChanged(Event event, const char[] name, bool dontBroadcast) { if (g_GameState != Get5State_None) { char cvarName[MAX_CVAR_LENGTH]; event.GetString("cvarname", cvarName, sizeof(cvarName)); @@ -1552,57 +1699,59 @@ public Action Event_CvarChanged(Event event, const char[] name, bool dontBroadca event.BroadcastDisabled = true; } } - - return Plugin_Continue; } -public void StartGame(bool knifeRound) { +static void StartGame(bool knifeRound) { LogDebug("StartGame"); - if (!IsTVEnabled()) { - LogMessage("GOTV demo could not be recorded since tv_enable is not set to 1"); - g_DemoFileName = ""; - } else { - char demoName[PLATFORM_MAX_PATH + 1]; - if (FormatCvarString(g_DemoNameFormatCvar, demoName, sizeof(demoName)) && Record(demoName)) { - Format(g_DemoFileName, sizeof(g_DemoFileName), "%s.dem", demoName); - LogMessage("Recording to %s", g_DemoFileName); - } else { - g_DemoFileName = ""; - } - } - - ExecCfg(g_LiveCfgCvar); if (knifeRound) { + ExecCfg(g_LiveCfgCvar); // live first, then apply and save knife cvars below LogDebug("StartGame: about to begin knife round"); ChangeState(Get5State_KnifeRound); if (g_KnifeChangedCvars != INVALID_HANDLE) { CloseCvarStorage(g_KnifeChangedCvars); } - g_KnifeChangedCvars = ExecuteAndSaveCvars(KNIFE_CONFIG); + char knifeConfig[PLATFORM_MAX_PATH]; + g_KnifeCfgCvar.GetString(knifeConfig, sizeof(knifeConfig)); + g_KnifeChangedCvars = ExecuteAndSaveCvars(knifeConfig); CreateTimer(1.0, StartKnifeRound); } else { - LogDebug("StartGame: about to go live"); - ChangeState(Get5State_GoingLive); - CreateTimer(3.0, StartGoingLive, _, TIMER_FLAG_NO_MAPCHANGE); + // If there is no knife round, we go directly to live, which loads the live config etc. on its + // own. + StartGoingLive(); } } -public Action Timer_PostKnife(Handle timer) { - if (g_KnifeChangedCvars != INVALID_HANDLE) { - RestoreCvars(g_KnifeChangedCvars, true); +static void SetServerStateOnStartup(bool force) { + if (g_GameState == Get5State_None) { + return; + } + if (!force && GetRealClientCount() != 1) { + // Only run on first client connect or if forced (during OnConfigsExecuted). + return; + } + // It shouldn't really be possible to end up here, as the server *should* reload the map anyway when first player + // joins, but as a safeguard we don't want to move a live game that's not pending a backup or map change into warmup + // on player connect. + if (!force && g_GameState == Get5State_Live && !g_WaitingForRoundBackup && !g_MapChangePending) { + return; + } + // If the server is in preveto when someone joins or the configs exec, it should remain in + // that state. This would happen if the a config with veto is loaded before someone joins the + // server. + if (g_GameState != Get5State_PreVeto) { + ChangeState(Get5State_Warmup); } - ExecCfg(g_WarmupCfgCvar); - EnsureIndefiniteWarmup(); + StartWarmup(); } -public Action StopDemo(Handle timer) { - StopRecording(); - return Plugin_Handled; -} +void ChangeState(Get5State state) { + if (g_GameState == state) { + LogDebug("Ignoring request to change game state. Already in state %d.", state); + return; + } -public void ChangeState(Get5State state) { g_GameStateCvar.IntValue = view_as(state); Get5GameStateChangedEvent event = new Get5GameStateChangedEvent(state, g_GameState); @@ -1618,7 +1767,7 @@ public void ChangeState(Get5State state) { g_GameState = state; } -public Action Command_Status(int client, int args) { +static Action Command_Status(int client, int args) { Get5Status status = new Get5Status(PLUGIN_VERSION, g_GameState, IsPaused()); if (g_GameState != Get5State_None) { @@ -1659,7 +1808,7 @@ static Get5StatusTeam GetTeamInfo(Get5Team team) { IsTeamReady(team), view_as(side), GetNumHumansOnTeam(side)); } -public bool FormatCvarString(ConVar cvar, char[] buffer, int len) { +bool FormatCvarString(ConVar cvar, char[] buffer, int len) { cvar.GetString(buffer, len); if (StrEqual(buffer, "")) { return false; @@ -1684,11 +1833,10 @@ public bool FormatCvarString(ConVar cvar, char[] buffer, int len) { strcopy(team2Str, sizeof(team2Str), g_TeamNames[Get5Team_2]); ReplaceString(team2Str, sizeof(team2Str), " ", "_"); - int mapNumber = g_TeamSeriesScores[Get5Team_1] + g_TeamSeriesScores[Get5Team_2] + 1; // MATCHTITLE must go first as it can contain other placeholders ReplaceString(buffer, len, "{MATCHTITLE}", g_MatchTitle, false); - ReplaceStringWithInt(buffer, len, "{MAPNUMBER}", mapNumber, false); - ReplaceStringWithInt(buffer, len, "{MAXMAPS}", MaxMapsToPlay(g_MapsToWin)); + ReplaceStringWithInt(buffer, len, "{MAPNUMBER}", Get5_GetMapNumber() + 1, false); + ReplaceStringWithInt(buffer, len, "{MAXMAPS}", g_NumberOfMapsInSeries, false); ReplaceString(buffer, len, "{MATCHID}", g_MatchID, false); ReplaceString(buffer, len, "{MAPNAME}", mapName, false); ReplaceStringWithInt(buffer, len, "{SERVERID}", g_ServerIdCvar.IntValue, false); @@ -1701,11 +1849,11 @@ public bool FormatCvarString(ConVar cvar, char[] buffer, int len) { // Formats a temp file path based ont he server id. The pattern parameter is expected to have a %d // token in it. -public void GetTempFilePath(char[] path, int len, const char[] pattern) { +void GetTempFilePath(char[] path, int len, const char[] pattern) { Format(path, len, pattern, g_ServerIdCvar.IntValue); } -public int GetRoundTime() { +int GetRoundTime() { int time = GetMilliSecondsPassedSince(g_RoundStartedTime); if (time < 0) { return 0; @@ -1713,7 +1861,7 @@ public int GetRoundTime() { return time; } -public void EventLogger_LogAndDeleteEvent(Get5Event event) { +void EventLogger_LogAndDeleteEvent(Get5Event event) { int options = g_PrettyPrintJsonCvar.BoolValue ? JSON_ENCODE_PRETTY : 0; int bufferSize = event.EncodeSize(options); @@ -1742,12 +1890,12 @@ public void EventLogger_LogAndDeleteEvent(Get5Event event) { json_cleanup_and_delete(event); } -stock void CheckForLatestVersion() { - +static void CheckForLatestVersion() { // both x.y.z-dev and x.y.z-abcdef contain a single dash, so we can look for that. g_RunningPrereleaseVersion = StrContains(PLUGIN_VERSION, "-", true) > -1; if (g_RunningPrereleaseVersion) { - LogMessage("Non-official Get5 version detected. Skipping update check. You may see this if you compiled Get5 \ + LogMessage( + "Non-official Get5 version detected. Skipping update check. You may see this if you compiled Get5 \ yourself or if you downloaded a pre-release for testing. If you are done testing, please download an official \ release version to remove this message."); return; @@ -1761,12 +1909,10 @@ release version to remove this message."); Handle req = SteamWorks_CreateHTTPRequest(k_EHTTPMethodGET, LATEST_VERSION_URL); SteamWorks_SetHTTPCallbacks(req, VersionCheckRequestCallback); SteamWorks_SendHTTPRequest(req); - } -stock int VersionCheckRequestCallback(Handle request, bool failure, bool requestSuccessful, - EHTTPStatusCode statusCode) { - +static int VersionCheckRequestCallback(Handle request, bool failure, bool requestSuccessful, + EHTTPStatusCode statusCode) { if (failure || !requestSuccessful) { LogError("Failed to check for Get5 update. HTTP error code: %d.", statusCode); delete request; @@ -1779,9 +1925,9 @@ stock int VersionCheckRequestCallback(Handle request, bool failure, bool request SteamWorks_GetHTTPResponseBodyData(request, response, responseSize); delete request; - // Since we're comparing against master, which always contains a -dev tag, we extract the version substring - // *before* that -dev tag (or whatever it might be). This *should* have been removed by the CI flow, so that official - // releases don't contain the -dev tag. + // Since we're comparing against master, which always contains a -dev tag, we extract the version + // substring *before* that -dev tag (or whatever it might be). This *should* have been removed by + // the CI flow, so that official releases don't contain the -dev tag. Regex versionRegex = new Regex("#define PLUGIN_VERSION \"(.+)-.+\""); RegexError rError; @@ -1805,12 +1951,13 @@ stock int VersionCheckRequestCallback(Handle request, bool failure, bool request LogDebug("Newest Get5 version from GitHub is: %s", newestVersionFound); g_NewerVersionAvailable = !StrEqual(PLUGIN_VERSION, newestVersionFound); if (g_NewerVersionAvailable) { - LogMessage("A newer version of Get5 is available. You are running %s while the latest version is %s.", PLUGIN_VERSION, newestVersionFound); + LogMessage( + "A newer version of Get5 is available. You are running %s while the latest version is %s.", + PLUGIN_VERSION, newestVersionFound); } else { LogMessage("Update check successful. Get5 is up-to-date (%s).", PLUGIN_VERSION); } } delete versionRegex; - } diff --git a/scripting/get5/backups.sp b/scripting/get5/backups.sp index 18a703d80..fd8cbca73 100644 --- a/scripting/get5/backups.sp +++ b/scripting/get5/backups.sp @@ -1,9 +1,15 @@ #define TEMP_MATCHCONFIG_BACKUP_PATTERN "get5_match_config_backup%d.txt" #define TEMP_VALVE_BACKUP_PATTERN "get5_temp_backup%d.txt" +#define TEMP_VALVE_NAMES_FILE_PATTERN "get5_names%d.txt" -public Action Command_LoadBackup(int client, int args) { +Action Command_LoadBackup(int client, int args) { if (!g_BackupSystemEnabledCvar.BoolValue) { - ReplyToCommand(client, "The backup system is disabled"); + ReplyToCommand(client, "The backup system is disabled."); + return Plugin_Handled; + } + + if (g_PendingSideSwap || InHalftimePhase()) { + ReplyToCommand(client, "You cannot load a backup during halftime."); return Plugin_Handled; } @@ -11,6 +17,7 @@ public Action Command_LoadBackup(int client, int args) { if (args >= 1 && GetCmdArg(1, path, sizeof(path))) { if (RestoreFromBackup(path)) { Get5_MessageToAll("%t", "BackupLoadedInfoMessage", path); + g_LastGet5BackupCvar.SetString(path); } else { ReplyToCommand(client, "Failed to load backup %s - check error logs", path); } @@ -21,7 +28,7 @@ public Action Command_LoadBackup(int client, int args) { return Plugin_Handled; } -public Action Command_ListBackups(int client, int args) { +Action Command_ListBackups(int client, int args) { if (!g_BackupSystemEnabledCvar.BoolValue) { ReplyToCommand(client, "The backup system is disabled"); return Plugin_Handled; @@ -34,30 +41,40 @@ public Action Command_ListBackups(int client, int args) { strcopy(matchID, sizeof(matchID), g_MatchID); } - char pattern[PLATFORM_MAX_PATH]; - Format(pattern, sizeof(pattern), "get5_backup_match%s", matchID); + char path[PLATFORM_MAX_PATH]; + g_RoundBackupPathCvar.GetString(path, sizeof(path)); + ReplaceString(path, sizeof(path), "{MATCHID}", matchID); - DirectoryListing files = OpenDirectory("."); + DirectoryListing files = OpenDirectory(strlen(path) > 0 ? path : "."); + bool foundBackups = false; if (files != null) { - char path[PLATFORM_MAX_PATH]; char backupInfo[256]; - - while (files.GetNext(path, sizeof(path))) { - if (StrContains(path, pattern) == 0) { - if (GetBackupInfo(path, backupInfo, sizeof(backupInfo))) { + char pattern[PLATFORM_MAX_PATH]; + Format(pattern, sizeof(pattern), "get5_backup_match%s", matchID); + + char filename[PLATFORM_MAX_PATH]; + while (files.GetNext(filename, sizeof(filename))) { + if (StrContains(filename, pattern) == 0) { + foundBackups = true; + Format(filename, sizeof(filename), "%s%s", path, filename); + if (GetBackupInfo(filename, backupInfo, sizeof(backupInfo))) { ReplyToCommand(client, backupInfo); } else { - ReplyToCommand(client, path); + ReplyToCommand(client, filename); } } } delete files; } + if (!foundBackups) { + ReplyToCommand(client, "Found no backup files matching the provided parameters."); + } + return Plugin_Handled; } -public bool GetBackupInfo(const char[] path, char[] info, int maxlength) { +static bool GetBackupInfo(const char[] path, char[] info, int maxlength) { KeyValues kv = new KeyValues("Backup"); if (!kv.ImportFromFile(path)) { LogError("Failed to find or read backup file \"%s\"", path); @@ -134,7 +151,84 @@ public bool GetBackupInfo(const char[] path, char[] info, int maxlength) { return true; } -public void WriteBackStructure(const char[] path) { +void WriteBackup() { + if (!g_BackupSystemEnabledCvar.BoolValue || IsDoingRestoreOrMapChange()) { + return; + } + + if (g_GameState != Get5State_Warmup && g_GameState != Get5State_KnifeRound && + g_GameState != Get5State_Live) { + LogDebug("Not writing backup for game state %d.", g_GameState); + return; // Only backup post-veto warmup, knife and live. + } + + char folder[PLATFORM_MAX_PATH]; + g_RoundBackupPathCvar.GetString(folder, sizeof(folder)); + ReplaceString(folder, sizeof(folder), "{MATCHID}", g_MatchID); + + int backupFolderLength = strlen(folder); + if (backupFolderLength > 0 && + (folder[0] == '/' || folder[0] == '.' || folder[backupFolderLength - 1] != '/' || + StrContains(folder, "//") != -1)) { + LogError( + "get5_backup_path must end with a slash and must not start with a slash or dot. It will be reset to an empty string! Current value: %s", + folder); + folder = ""; + g_RoundBackupPathCvar.SetString(folder, false, false); + } else { + CreateBackupFolderStructure(folder); + } + + char path[PLATFORM_MAX_PATH]; + if (g_GameState == Get5State_Live) { + Format(path, sizeof(path), "%sget5_backup_match%s_map%d_round%d.cfg", folder, g_MatchID, + g_MapNumber, g_RoundNumber); + } else { + Format(path, sizeof(path), "%sget5_backup_match%s_map%d_prelive.cfg", folder, g_MatchID, + g_MapNumber); + } + LogDebug("Writing backup to %s", path); + WriteBackupStructure(path); + g_LastGet5BackupCvar.SetString(path); +} + +static bool CreateDirectoryWithPermissions(const char[] directory) { + LogDebug("Creating directory: %s", directory); + return CreateDirectory(directory, // sets 777 permissions. + FPERM_U_READ | FPERM_U_WRITE | FPERM_U_EXEC | FPERM_G_READ | + FPERM_G_WRITE | FPERM_G_EXEC | FPERM_O_READ | FPERM_O_WRITE | + FPERM_O_EXEC); +} + +static bool CreateBackupFolderStructure(const char[] path) { + if (strlen(path) == 0 || DirExists(path)) { + return true; + } + + LogDebug("Creating backup directory %s because it does not exist.", path); + char folders[16][PLATFORM_MAX_PATH]; // {folder1, folder2, etc} + char fullFolderPath[PLATFORM_MAX_PATH] = + ""; // initially empty, but we append every time a folder is created/verified + char currentFolder[PLATFORM_MAX_PATH]; // shorthand for folders[i] + + ExplodeString(path, "/", folders, sizeof(folders), PLATFORM_MAX_PATH, true); + for (int i = 0; i < sizeof(folders); i++) { + currentFolder = folders[i]; + if (strlen(currentFolder) == + 0) { // as the loop is a fixed size, we stop when there are no more pieces. + break; + } + // Append the current folder to the full path + Format(fullFolderPath, sizeof(fullFolderPath), "%s%s/", fullFolderPath, currentFolder); + if (!DirExists(fullFolderPath) && !CreateDirectoryWithPermissions(fullFolderPath)) { + LogError("Failed to create or verify existence of directory: %s", fullFolderPath); + return false; + } + } + return true; +} + +static void WriteBackupStructure(const char[] path) { KeyValues kv = new KeyValues("Backup"); char timeString[PLATFORM_MAX_PATH]; FormatTime(timeString, sizeof(timeString), "%Y-%m-%d %H:%M:%S", GetTime()); @@ -164,6 +258,15 @@ public void WriteBackStructure(const char[] path) { kv.SetNum("team1_series_score", g_TeamSeriesScores[Get5Team_1]); kv.SetNum("team2_series_score", g_TeamSeriesScores[Get5Team_2]); + kv.SetNum("series_draw", g_TeamSeriesScores[Get5Team_None]); + + kv.SetNum("team1_tac_pauses_used", g_TacticalPausesUsed[Get5Team_1]); + kv.SetNum("team2_tac_pauses_used", g_TacticalPausesUsed[Get5Team_2]); + kv.SetNum("team1_tech_pauses_used", g_TechnicalPausesUsed[Get5Team_1]); + kv.SetNum("team2_tech_pauses_used", g_TechnicalPausesUsed[Get5Team_2]); + kv.SetNum("team1_pause_time_used", g_TacticalPauseTimeUsed[Get5Team_1]); + kv.SetNum("team2_pause_time_used", g_TacticalPauseTimeUsed[Get5Team_2]); + // Write original maplist. kv.JumpToKey("maps", true); for (int i = 0; i < g_MapsToPlay.Length; i++) { @@ -191,18 +294,21 @@ public void WriteBackStructure(const char[] path) { WriteMatchToKv(kv); kv.GoBack(); - // Write valve's backup format into the file. - char lastBackup[PLATFORM_MAX_PATH]; - ConVar lastBackupCvar = FindConVar("mp_backup_round_file_last"); - if (g_GameState == Get5State_Live && lastBackupCvar != null) { - lastBackupCvar.GetString(lastBackup, sizeof(lastBackup)); - KeyValues valveBackup = new KeyValues("valve_backup"); - if (valveBackup.ImportFromFile(lastBackup)) { - kv.JumpToKey("valve_backup", true); - KvCopySubkeys(valveBackup, kv); - kv.GoBack(); + if (g_GameState == Get5State_Live) { + // Write valve's backup format into the file. This only applies to live rounds, as any pre-live + // backups should just restart the game to the knife round. + char lastBackup[PLATFORM_MAX_PATH]; + ConVar lastBackupCvar = FindConVar("mp_backup_round_file_last"); + if (lastBackupCvar != null) { + lastBackupCvar.GetString(lastBackup, sizeof(lastBackup)); + KeyValues valveBackup = new KeyValues("valve_backup"); + if (valveBackup.ImportFromFile(lastBackup)) { + kv.JumpToKey("valve_backup", true); + KvCopySubkeys(valveBackup, kv); + kv.GoBack(); + } + delete valveBackup; } - delete valveBackup; } // Write the get5 stats into the file. @@ -214,7 +320,7 @@ public void WriteBackStructure(const char[] path) { delete kv; } -public bool RestoreFromBackup(const char[] path) { +bool RestoreFromBackup(const char[] path, bool restartRecording = true) { KeyValues kv = new KeyValues("Backup"); if (!kv.ImportFromFile(path)) { LogError("Failed to read backup file \"%s\"", path); @@ -222,6 +328,13 @@ public bool RestoreFromBackup(const char[] path) { return false; } + if (restartRecording) { + // We must stop recording when loading a backup, and we must do it before we load the match + // config, or the g_MatchID variable will be incorrect. This is suppressed when using the !stop + // command. + StopRecording(); + } + if (kv.JumpToKey("Match")) { char tempBackupFile[PLATFORM_MAX_PATH]; GetTempFilePath(tempBackupFile, sizeof(tempBackupFile), TEMP_MATCHCONFIG_BACKUP_PATTERN); @@ -229,11 +342,27 @@ public bool RestoreFromBackup(const char[] path) { if (!LoadMatchConfig(tempBackupFile, true)) { delete kv; LogError("Could not restore from match config \"%s\"", tempBackupFile); + // If the backup load fails, all the game configs will have been reset by LoadMatchConfig, + // but the game state won't. This ensures we don't end up a in a "live" state with no get5 + // variables set, which would prevent a call to load a new match. + ChangeState(Get5State_None); return false; } kv.GoBack(); } + if (g_GameState != Get5State_Live) { + // This isn't perfect, but it's better than resetting all pauses used to zero in cases of + // restore on a new server. If restoring while live, we just retain the current pauses used, as + // they should be the "most correct". + g_TacticalPausesUsed[Get5Team_1] = kv.GetNum("team1_tac_pauses_used", 0); + g_TacticalPausesUsed[Get5Team_2] = kv.GetNum("team2_tac_pauses_used", 0); + g_TechnicalPausesUsed[Get5Team_1] = kv.GetNum("team1_tech_pauses_used", 0); + g_TechnicalPausesUsed[Get5Team_2] = kv.GetNum("team2_tech_pauses_used", 0); + g_TacticalPauseTimeUsed[Get5Team_1] = kv.GetNum("team1_pause_time_used", 0); + g_TacticalPauseTimeUsed[Get5Team_2] = kv.GetNum("team2_pause_time_used", 0); + } + kv.GetString("matchid", g_MatchID, sizeof(g_MatchID)); g_GameState = view_as(kv.GetNum("gamestate")); @@ -246,39 +375,44 @@ public bool RestoreFromBackup(const char[] path) { g_TeamSeriesScores[Get5Team_1] = kv.GetNum("team1_series_score"); g_TeamSeriesScores[Get5Team_2] = kv.GetNum("team2_series_score"); + // This ensures that the MapNumber logic correctly calculates the map number when there have been + // draws. + g_TeamSeriesScores[Get5Team_None] = kv.GetNum("series_draw", 0); + + // Immediately set map number global var to ensure anything below doesn't break. + g_MapNumber = Get5_GetMapNumber(); + char mapName[PLATFORM_MAX_PATH]; - if (g_GameState > Get5State_Veto) { - if (kv.JumpToKey("maps")) { - g_MapsToPlay.Clear(); - g_MapSides.Clear(); - if (kv.GotoFirstSubKey(false)) { - do { - kv.GetSectionName(mapName, sizeof(mapName)); - SideChoice sides = view_as(kv.GetNum(NULL_STRING)); - g_MapsToPlay.PushString(mapName); - g_MapSides.Push(sides); - } while (kv.GotoNextKey(false)); - kv.GoBack(); - } + if (kv.JumpToKey("maps")) { + g_MapsToPlay.Clear(); + g_MapSides.Clear(); + if (kv.GotoFirstSubKey(false)) { + do { + kv.GetSectionName(mapName, sizeof(mapName)); + SideChoice sides = view_as(kv.GetNum(NULL_STRING)); + g_MapsToPlay.PushString(mapName); + g_MapSides.Push(sides); + } while (kv.GotoNextKey(false)); kv.GoBack(); } + kv.GoBack(); + } - if (kv.JumpToKey("map_scores")) { - if (kv.GotoFirstSubKey()) { - do { - char buf[32]; - kv.GetSectionName(buf, sizeof(buf)); - int map = StringToInt(buf); - - int t1 = kv.GetNum("team1"); - int t2 = kv.GetNum("team2"); - g_TeamScoresPerMap.Set(map, t1, view_as(Get5Team_1)); - g_TeamScoresPerMap.Set(map, t2, view_as(Get5Team_2)); - } while (kv.GotoNextKey()); - kv.GoBack(); - } + if (kv.JumpToKey("map_scores")) { + if (kv.GotoFirstSubKey()) { + do { + char buf[32]; + kv.GetSectionName(buf, sizeof(buf)); + int map = StringToInt(buf); + + int t1 = kv.GetNum("team1"); + int t2 = kv.GetNum("team2"); + g_TeamScoresPerMap.Set(map, t1, view_as(Get5Team_1)); + g_TeamScoresPerMap.Set(map, t2, view_as(Get5Team_2)); + } while (kv.GotoNextKey()); kv.GoBack(); } + kv.GoBack(); } if (kv.JumpToKey("stats")) { @@ -287,28 +421,51 @@ public bool RestoreFromBackup(const char[] path) { kv.GoBack(); } - char tempValveBackup[PLATFORM_MAX_PATH]; - GetTempFilePath(tempValveBackup, sizeof(tempValveBackup), TEMP_VALVE_BACKUP_PATTERN); + // When loading pre-live, there is no Valve backup, so we assume -1. + g_WaitingForRoundBackup = false; + int roundNumberRestoredTo = -1; if (kv.JumpToKey("valve_backup")) { - g_SavedValveBackup = true; + g_WaitingForRoundBackup = true; + char tempValveBackup[PLATFORM_MAX_PATH]; + GetTempFilePath(tempValveBackup, sizeof(tempValveBackup), TEMP_VALVE_BACKUP_PATTERN); kv.ExportToFile(tempValveBackup); + roundNumberRestoredTo = kv.GetNum("round", 0); kv.GoBack(); - } else { - g_SavedValveBackup = false; } char currentMap[PLATFORM_MAX_PATH]; GetCurrentMap(currentMap, sizeof(currentMap)); char currentSeriesMap[PLATFORM_MAX_PATH]; - g_MapsToPlay.GetString(Get5_GetMapNumber(), currentSeriesMap, sizeof(currentSeriesMap)); + g_MapsToPlay.GetString(g_MapNumber, currentSeriesMap, sizeof(currentSeriesMap)); if (!StrEqual(currentMap, currentSeriesMap)) { - ChangeMap(currentSeriesMap, 1.0); - g_WaitingForRoundBackup = (g_GameState >= Get5State_Live); - + // We don't need to assign players if changing map; this will be done when the players rejoin. + // If a map is to be changed, we want to suppress all stats events immediately, as the + // Get5_OnBackupRestore is called now and we don't want events firing after this until the game + // is live again. + ChangeMap(currentSeriesMap, 3.0); } else { - RestoreGet5Backup(); + // We must assign players to their teams. This is normally done inside LoadMatchConfig, but + // since we need the team sides to be applied from the backup, we skip it then and do it here. + LOOP_CLIENTS(i) { + if (IsPlayer(i)) { + CheckClientTeam(i); + } + } + if (g_WaitingForRoundBackup) { + // Same map, but round restore with a Valve backup; do normal restore immediately with no + // ready-up. + RestoreGet5Backup(restartRecording); + } else { + // We are restarting to the same map for prelive; just go back into warmup and let players + // ready-up again. + ResetReadyStatus(); + UnpauseGame(Get5Team_None); + ChangeState(Get5State_Warmup); + ExecCfg(g_WarmupCfgCvar); + StartWarmup(); + } } delete kv; @@ -316,7 +473,7 @@ public bool RestoreFromBackup(const char[] path) { LogDebug("Calling Get5_OnBackupRestore()"); Get5BackupRestoredEvent backupEvent = - new Get5BackupRestoredEvent(g_MatchID, Get5_GetMapNumber(), path); + new Get5BackupRestoredEvent(g_MatchID, g_MapNumber, roundNumberRestoredTo, path); Call_StartForward(g_OnBackupRestore); Call_PushCell(backupEvent); @@ -327,92 +484,107 @@ public bool RestoreFromBackup(const char[] path) { return true; } -public void RestoreGet5Backup() { - // This variable is reset on a timer since the implementation of the - // mp_backup_restore_load_file doesn't do everything in one frame. - g_DoingBackupRestoreNow = true; +void RestoreGet5Backup(bool restartRecording = true) { + // If you load a backup during a live round, the game might get stuck if there are only bots + // remaining and no players are alive. Other stuff will probably also go wrong, so we just reset + // the game before loading the backup to avoid any weird edge-cases. + if (!InWarmup()) { + RestartGame(); + } ExecCfg(g_LiveCfgCvar); - - if (g_SavedValveBackup) { - ChangeState(Get5State_Live); - SetMatchTeamCvars(); - ExecuteMatchConfigCvars(); - SetMatchRestartDelay(); - - // There are some timing issues leading to incorrect score when restoring matches in second - // half. Doing the restore on a timer - CreateTimer(1.0, Time_StartRestore); - } else { - SetStartingTeams(); - SetMatchTeamCvars(); - ExecuteMatchConfigCvars(); - for (int i = 1; i <= MaxClients; i++) { - if (IsPlayer(i)) - CheckClientTeam(i); - } - - if (g_GameState == Get5State_Live) { - EndWarmup(); - EndWarmup(); - ServerCommand("mp_restartgame 5"); - PauseGame(Get5Team_None, Get5PauseType_Backup); - if (g_CoachingEnabledCvar.BoolValue) { - CreateTimer(6.0, Timer_SwapCoaches); - } - } else { - EnsureIndefiniteWarmup(); - } - - g_DoingBackupRestoreNow = false; + PauseGame(Get5Team_None, Get5PauseType_Backup); + g_DoingBackupRestoreNow = true; // reset after the backup has completed, suppresses various + // events and hooks until then. + g_WaitingForRoundBackup = false; + CreateTimer(1.5, Timer_StartRestore); + if (restartRecording) { + // Since a backup command forces the recording to stop, we restart it here once the backup has + // completed. We have to do this on a delay, as when loading from a live game, the backup will + // already be recording and must flush before a new record command can be issued. This is + // suppressed when using the !stop command! + CreateTimer(3.0, Timer_StartRecordingAfterBackup, _, TIMER_FLAG_NO_MAPCHANGE); } } -public Action Timer_SwapCoaches(Handle timer) { - for (int i = 1; i <= MaxClients; i++) { - if (IsAuthedPlayer(i)) { - CheckIfClientCoachingAndMoveToCoach(i, Get5Team_1); - CheckIfClientCoachingAndMoveToCoach(i, Get5Team_2); - } +static Action Timer_StartRecordingAfterBackup(Handle timer) { + if (g_GameState != Get5State_Live) { + return; } + StartRecording(); } -public Action Time_StartRestore(Handle timer) { - PauseGame(Get5Team_None, Get5PauseType_Backup); - +static Action Timer_StartRestore(Handle timer) { + ChangeState(Get5State_Live); char tempValveBackup[PLATFORM_MAX_PATH]; GetTempFilePath(tempValveBackup, sizeof(tempValveBackup), TEMP_VALVE_BACKUP_PATTERN); ServerCommand("mp_backup_restore_load_file \"%s\"", tempValveBackup); - CreateTimer(0.1, Timer_FinishBackup); + CreateTimer(0.5, Timer_FinishBackup); + + // We need to fire the OnRoundStarted event manually, as it will be suppressed during backups and + // won't fire while g_DoingBackupRestoreNow is true. + KeyValues kv = new KeyValues("Backup"); + if (kv.ImportFromFile(tempValveBackup)) { + Get5RoundStartedEvent startEvent = + new Get5RoundStartedEvent(g_MatchID, g_MapNumber, kv.GetNum("round", 0)); + LogDebug("Calling Get5_OnRoundStart() via backup."); + Call_StartForward(g_OnRoundStart); + Call_PushCell(startEvent); + Call_Finish(); + EventLogger_LogAndDeleteEvent(startEvent); + } + delete kv; } -public Action Timer_FinishBackup(Handle timer) { - if (g_CoachingEnabledCvar.BoolValue) { - // If we are coaching we want to ensure our - // coaches get moved back onto the team. - // We cannot trust Valve's system as a disconnected - // player will count as a "player" and not be placed - // in the coach slot. So, we cannot enable warmup during - // the round restore process if using a Valve backup. - CreateTimer(0.5, Timer_SwapCoaches); +static Action Timer_FinishBackup(Handle timer) { + // This ensures that coaches are moved to their slots. + LOOP_CLIENTS(i) { + if (IsPlayer(i)) { + CheckClientTeam(i); + } } g_DoingBackupRestoreNow = false; + // Delete the temporary backup file we just wrote and restored from. + char tempValveBackup[PLATFORM_MAX_PATH]; + GetTempFilePath(tempValveBackup, sizeof(tempValveBackup), TEMP_VALVE_BACKUP_PATTERN); + if (DeleteFile(tempValveBackup)) { + LogDebug("Deleted temp valve backup file: %s", tempValveBackup); + } else { + LogDebug("Failed to delete temp valve backup file: %s", tempValveBackup); + } } -public void DeleteOldBackups() { +void DeleteOldBackups() { int maxTimeDifference = g_MaxBackupAgeCvar.IntValue; if (maxTimeDifference <= 0) { + LogDebug("Backups are not being deleted as get5_max_backup_age is 0."); return; } - DirectoryListing files = OpenDirectory("."); + char path[PLATFORM_MAX_PATH]; + g_RoundBackupPathCvar.GetString(path, sizeof(path)); + + if (StrContains(path, "{MATCHID}") != -1) { + LogError( + "Automatic backup deletion cannot be performed when get5_backup_path contains the {MATCHID} variable."); + return; + } + + DirectoryListing files = OpenDirectory(strlen(path) > 0 ? path : "."); if (files != null) { - char path[PLATFORM_MAX_PATH]; - while (files.GetNext(path, sizeof(path))) { - if (StrContains(path, "get5_backup_") == 0 && - GetTime() - GetFileTime(path, FileTime_LastChange) >= maxTimeDifference) { - DeleteFile(path); + LogDebug("Searching '%s' for expired backups...", path); + char filename[PLATFORM_MAX_PATH]; + while (files.GetNext(filename, sizeof(filename))) { + if (StrContains(filename, "get5_backup_") == 0) { + Format(filename, sizeof(filename), "%s%s", path, filename); + if (GetTime() - GetFileTime(filename, FileTime_LastChange) >= maxTimeDifference) { + if (DeleteFileIfExists(filename)) { + LogDebug("Deleted '%s' as it was older than %d seconds.", filename, maxTimeDifference); + } + } } } delete files; + } else { + LogError("Failed to list contents of directory '%s' for backup deletion.", path); } } diff --git a/scripting/get5/chatcommands.sp b/scripting/get5/chatcommands.sp index f7305d119..39ca648a0 100644 --- a/scripting/get5/chatcommands.sp +++ b/scripting/get5/chatcommands.sp @@ -1,4 +1,4 @@ -public void AddAliasedCommand(const char[] command, ConCmd callback, const char[] description) { +void AddAliasedCommand(const char[] command, ConCmd callback, const char[] description) { char smCommandBuffer[COMMAND_LENGTH]; Format(smCommandBuffer, sizeof(smCommandBuffer), "sm_%s", command); RegConsoleCmd(smCommandBuffer, callback, description); @@ -8,7 +8,7 @@ public void AddAliasedCommand(const char[] command, ConCmd callback, const char[ AddChatAlias(dotCommandBuffer, smCommandBuffer); } -public void AddChatAlias(const char[] alias, const char[] command) { +static void AddChatAlias(const char[] alias, const char[] command) { // Don't allow duplicate aliases to be added. if (g_ChatAliases.FindString(alias) == -1) { g_ChatAliases.PushString(alias); @@ -16,7 +16,7 @@ public void AddChatAlias(const char[] alias, const char[] command) { } } -public void CheckForChatAlias(int client, const char[] command, const char[] sArgs) { +void CheckForChatAlias(int client, const char[] sArgs) { // No chat aliases are needed if the game isn't setup at all. if (g_GameState == Get5State_None) { return; diff --git a/scripting/get5/debug.sp b/scripting/get5/debug.sp index 59dc1f1fb..bfc645fca 100644 --- a/scripting/get5/debug.sp +++ b/scripting/get5/debug.sp @@ -1,7 +1,7 @@ // TODO: Also try to write the original match config file. // Also consider the last K lines from the most recent errors_* file? -public Action Command_DebugInfo(int client, int args) { +Action Command_DebugInfo(int client, int args) { char path[PLATFORM_MAX_PATH + 1]; if (args == 0 || !GetCmdArg(1, path, sizeof(path))) { @@ -94,13 +94,21 @@ static void AddGlobalStateInfo(File f) { f.WriteLine("g_MatchID = %s", g_MatchID); f.WriteLine("g_RoundNumber = %d", g_RoundNumber); f.WriteLine("g_MapsToWin = %d", g_MapsToWin); - f.WriteLine("g_BO2Match = %d", g_BO2Match); f.WriteLine("g_LastVetoTeam = %d", g_LastVetoTeam); WriteArrayList(f, "g_MapPoolList", g_MapPoolList); WriteArrayList(f, "g_MapsToPlay", g_MapsToPlay); WriteArrayList(f, "g_MapsLeftInVetoPool", g_MapsLeftInVetoPool); - // TODO: write g_MapSides (it's not a string so WriteArrayList doesn't work). - + f.WriteLine("Defined map sides:"); + for (int i = 0; i < g_MapSides.Length; i++) { + SideChoice c = g_MapSides.Get(i); + if (c == SideChoice_Team1CT) { + f.WriteLine("g_MapSides(%d) = team1_ct", i); + } else if (c == SideChoice_Team1T) { + f.WriteLine("g_MapSides(%d) = team1_t", i); + } else { + f.WriteLine("g_MapSides(%d) = knife", i); + } + } f.WriteLine("g_MatchTitle = %s", g_MatchTitle); f.WriteLine("g_PlayersPerTeam = %d", g_PlayersPerTeam); f.WriteLine("g_CoachesPerTeam = %d", g_CoachesPerTeam); @@ -109,12 +117,12 @@ static void AddGlobalStateInfo(File f) { f.WriteLine("g_SkipVeto = %d", g_SkipVeto); f.WriteLine("g_MatchSideType = %d", g_MatchSideType); f.WriteLine("g_InScrimMode = %d", g_InScrimMode); + f.WriteLine("g_SeriesCanClinch = %d", g_SeriesCanClinch); f.WriteLine("g_HasKnifeRoundStarted = %d", g_HasKnifeRoundStarted); f.WriteLine("g_MapChangePending = %d", g_MapChangePending); f.WriteLine("g_PendingSideSwap = %d", g_PendingSideSwap); f.WriteLine("g_WaitingForRoundBackup = %d", g_WaitingForRoundBackup); - f.WriteLine("g_SavedValveBackup = %d", g_SavedValveBackup); f.WriteLine("g_DoingBackupRestoreNow = %d", g_DoingBackupRestoreNow); f.WriteLine("g_ReadyTimeWaitingUsed = %d", g_ReadyTimeWaitingUsed); f.WriteLine("g_PausingTeam = %d", g_PausingTeam); diff --git a/scripting/get5/get5menu.sp b/scripting/get5/get5menu.sp index 61ed8a359..a36e9b4de 100644 --- a/scripting/get5/get5menu.sp +++ b/scripting/get5/get5menu.sp @@ -1,7 +1,7 @@ // TODO: Add translations for this. // TODO: Add admin top menu integration. -public Action Command_Get5AdminMenu(int client, int args) { +Action Command_Get5AdminMenu(int client, int args) { Menu menu = new Menu(AdminMenuHandler); menu.SetTitle("Get5 Admin Menu"); @@ -33,7 +33,7 @@ static int EnabledIf(bool cond) { return cond ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED; } -public int AdminMenuHandler(Menu menu, MenuAction action, int param1, int param2) { +static int AdminMenuHandler(Menu menu, MenuAction action, int param1, int param2) { if (action == MenuAction_Select) { int client = param1; char infoString[64]; @@ -52,13 +52,13 @@ public int AdminMenuHandler(Menu menu, MenuAction action, int param1, int param2 } } -public void GiveRingerMenu(int client) { +static void GiveRingerMenu(int client) { Menu menu = new Menu(RingerMenuHandler); menu.SetTitle("Switch scrim team status"); menu.ExitButton = true; menu.ExitBackButton = true; - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (IsPlayer(i)) { char infoString[64]; IntToString(GetClientSerial(i), infoString, sizeof(infoString)); @@ -70,7 +70,7 @@ public void GiveRingerMenu(int client) { menu.Display(client, MENU_TIME_FOREVER); } -public int RingerMenuHandler(Menu menu, MenuAction action, int param1, int param2) { +static int RingerMenuHandler(Menu menu, MenuAction action, int param1, int param2) { if (action == MenuAction_Select) { int client = param1; char infoString[64]; diff --git a/scripting/get5/goinglive.sp b/scripting/get5/goinglive.sp index b2742669b..b808b7776 100644 --- a/scripting/get5/goinglive.sp +++ b/scripting/get5/goinglive.sp @@ -1,27 +1,8 @@ -/** Begins the LO3 process. **/ -public Action StartGoingLive(Handle timer) { +void StartGoingLive() { LogDebug("StartGoingLive"); ExecCfg(g_LiveCfgCvar); - SetMatchTeamCvars(); - ExecuteMatchConfigCvars(); - // Force kill the warmup if we (still) need to. - Get5_MessageToAll("%t", "MatchBeginInSecondsInfoMessage", g_LiveCountdownTimeCvar.IntValue); - if (InWarmup()) { - EndWarmup(g_LiveCountdownTimeCvar.IntValue); - } else { - RestartGame(g_LiveCountdownTimeCvar.IntValue); - } - - // Always disable sv_cheats! - ServerCommand("sv_cheats 0"); - - // Delayed an extra 5 seconds for the final 3-second countdown - // the game uses after the origina countdown. - float delay = float(5 + g_LiveCountdownTimeCvar.IntValue); - CreateTimer(delay, MatchLive); - - Get5GoingLiveEvent liveEvent = new Get5GoingLiveEvent(g_MatchID, Get5_GetMapNumber()); + Get5GoingLiveEvent liveEvent = new Get5GoingLiveEvent(g_MatchID, g_MapNumber); LogDebug("Calling Get5_OnGoingLive()"); @@ -31,53 +12,61 @@ public Action StartGoingLive(Handle timer) { EventLogger_LogAndDeleteEvent(liveEvent); + ChangeState(Get5State_GoingLive); + + // This ensures that we can send send the game to warmup and count down *even if* someone had put + // "mp_warmup_end", or something else that would mess up warmup, in their live config, which they + // shouldn't. But we can't be sure. + CreateTimer(1.0, Timer_GoToLiveAfterWarmupCountdown, _, TIMER_FLAG_NO_MAPCHANGE); +} + +static Action Timer_GoToLiveAfterWarmupCountdown(Handle timer) { + if (g_GameState != Get5State_GoingLive) { + return Plugin_Handled; // super defensive race-condition check. + } + // Always disable sv_cheats! + ServerCommand("sv_cheats 0"); + // Ensure we're in warmup and counting down to live. Round_PreStart handles the rest. + int countdown = g_LiveCountdownTimeCvar.IntValue; + if (countdown < 5) { + countdown = + 5; // ensures that a cvar countdown value of 0 does not leave the game forever in warmup. + } + Get5_MessageToAll("%t", "MatchBeginInSecondsInfoMessage", countdown); + StartWarmup(countdown); + LogDebug("Started warmup countdown to live in %d seconds.", countdown); return Plugin_Handled; } -public Action MatchLive(Handle timer) { - if (g_GameState == Get5State_None) { +Action Timer_MatchLive(Handle timer) { + if (g_GameState != Get5State_Live) { return Plugin_Handled; } - // Reset match config cvars. The problem is that when they are first - // set in StartGoingLive is that setting them right after executing the - // live config causes the live config values to get used for some reason - // (asynchronous command execution/cvar setting?), so they're set again - // to be sure. - SetMatchTeamCvars(); - ExecuteMatchConfigCvars(); - - // We force the match end-delay to extend for the duration of the GOTV broadcast here. - g_PendingSideSwap = false; - SetMatchRestartDelay(); - - for (int i = 0; i < 5; i++) { - Get5_MessageToAll("%t", "MatchIsLiveInfoMessage"); + AnnouncePhaseChange("%t", "MatchIsLiveInfoMessage"); + + if (g_PrintUpdateNoticeCvar.BoolValue) { + if (g_RunningPrereleaseVersion) { + char conVarName[64]; + g_PrintUpdateNoticeCvar.GetName(conVarName, sizeof(conVarName)); + FormatCvarName(conVarName, sizeof(conVarName), conVarName); + Get5_MessageToAll("%t", "PrereleaseVersionWarning", PLUGIN_VERSION, conVarName); + } else if (g_NewerVersionAvailable) { + Get5_MessageToAll("%t", "NewVersionAvailable", GET5_GITHUB_PAGE); + } } + /** + * Please do not change this. Thousands of uncompensated hours were poured into making this + * plugin. Claiming it as your own because you made slight modifications to it is not cool. If you + * have suggestions, bug reports or feature requests, please see GitHub or join our Discord: + * https://splewis.github.io/get5/community/ Thanks in advance! + */ char tag[64]; g_MessagePrefixCvar.GetString(tag, sizeof(tag)); if (!StrEqual(tag, DEFAULT_TAG)) { - Get5_MessageToAll("%t", "MatchPoweredBy"); - } - - if (!g_PrintUpdateNoticeCvar.BoolValue) { - return Plugin_Handled; - } - - if (g_RunningPrereleaseVersion) { - char conVarName[64]; - g_PrintUpdateNoticeCvar.GetName(conVarName, sizeof(conVarName)); - Get5_MessageToAll("%t", "PrereleaseVersionWarning", PLUGIN_VERSION, conVarName); - } else if (g_NewerVersionAvailable) { - Get5_MessageToAll("%t", "NewVersionAvailable", GET5_GITHUB_PAGE); + Get5_MessageToAll("Powered by {YELLOW}Get5"); } return Plugin_Handled; } - -public void SetMatchRestartDelay() { - ConVar mp_match_restart_delay = FindConVar("mp_match_restart_delay"); - int delay = GetTvDelay() + MATCH_END_DELAY_AFTER_TV + 5; - SetConVarInt(mp_match_restart_delay, delay); -} diff --git a/scripting/get5/jsonhelpers.sp b/scripting/get5/jsonhelpers.sp index c24e3537b..3f3d9bf79 100644 --- a/scripting/get5/jsonhelpers.sp +++ b/scripting/get5/jsonhelpers.sp @@ -123,7 +123,7 @@ stock int AddJsonAuthsToList(JSON_Object json, const char[] key, ArrayList list, data.GetString(k, name, sizeof(name)); char steam64[AUTH_LENGTH]; if (ConvertAuthToSteam64(k, steam64)) { - Get5_SetPlayerName(steam64, name); + Get5_SetPlayerName(steam64, name, true); list.PushString(steam64); count++; } diff --git a/scripting/get5/kniferounds.sp b/scripting/get5/kniferounds.sp index 2276dd177..be5848144 100644 --- a/scripting/get5/kniferounds.sp +++ b/scripting/get5/kniferounds.sp @@ -1,6 +1,8 @@ -public Action StartKnifeRound(Handle timer) { +Action StartKnifeRound(Handle timer) { g_HasKnifeRoundStarted = false; - g_PendingSideSwap = false; + + // Removes ready tags + SetMatchTeamCvars(); Get5_MessageToAll("%t", "KnifeIn5SecInfoMessage"); if (InWarmup()) { @@ -9,14 +11,13 @@ public Action StartKnifeRound(Handle timer) { RestartGame(5); } - CreateTimer(10.0, Timer_AnnounceKnife); + g_KnifeCountdownTimer = CreateTimer(10.0, Timer_AnnounceKnife); return Plugin_Handled; } -public Action Timer_AnnounceKnife(Handle timer) { - for (int i = 0; i < 5; i++) { - Get5_MessageToAll("%t", "KnifeInfoMessage"); - } +static Action Timer_AnnounceKnife(Handle timer) { + g_KnifeCountdownTimer = INVALID_HANDLE; + AnnouncePhaseChange("{GREEN}%t", "KnifeInfoMessage"); Get5KnifeRoundStartedEvent knifeEvent = new Get5KnifeRoundStartedEvent(g_MatchID, g_MapNumber); @@ -38,16 +39,15 @@ static void PerformSideSwap(bool swap) { g_TeamSide[Get5Team_2] = g_TeamSide[Get5Team_1]; g_TeamSide[Get5Team_1] = tmp; - for (int i = 1; i <= MaxClients; i++) { - if (IsValidClient(i)) { - int team = GetClientTeam(i); - if (team == CS_TEAM_T) { - SwitchPlayerTeam(i, CS_TEAM_CT); - } else if (team == CS_TEAM_CT) { - SwitchPlayerTeam(i, CS_TEAM_T); - } else if (IsClientCoaching(i)) { - int correctTeam = Get5TeamToCSTeam(GetClientMatchTeam(i)); - UpdateCoachTarget(i, correctTeam); + LOOP_CLIENTS(i) { + if (IsValidClient(i) && !IsClientSourceTV(i)) { + if (IsFakeClient(i)) { + // Because bots never have an assigned team, they won't be moved around by + // CheckClientTeam. We kick them to prevent one team from having too many players. They + // will rejoin if defined in the live config. + KickClient(i); + } else { + CheckClientTeam(i, false); } } } @@ -55,9 +55,9 @@ static void PerformSideSwap(bool swap) { // that way set starting teams won't swap on round 0, // since a temp valve backup does not exist. if (g_TeamSide[Get5Team_1] == CS_TEAM_CT) - g_MapSides.Set(Get5_GetMapNumber(), SideChoice_Team1CT); + g_MapSides.Set(g_MapNumber, SideChoice_Team1CT); else - g_MapSides.Set(Get5_GetMapNumber(), SideChoice_Team1T); + g_MapSides.Set(g_MapNumber, SideChoice_Team1T); } else { g_TeamSide[Get5Team_1] = TEAM1_STARTING_SIDE; g_TeamSide[Get5Team_2] = TEAM2_STARTING_SIDE; @@ -68,7 +68,7 @@ static void PerformSideSwap(bool swap) { SetMatchTeamCvars(); } -public void EndKnifeRound(bool swap) { +static void EndKnifeRound(bool swap) { PerformSideSwap(swap); Get5KnifeRoundWonEvent knifeEvent = @@ -81,10 +81,14 @@ public void EndKnifeRound(bool swap) { Call_PushCell(knifeEvent); Call_Finish(); - EventLogger_LogAndDeleteEvent(knifeEvent); + if (g_KnifeDecisionTimer != INVALID_HANDLE) { + LogDebug("Stopped knife decision timer as a choice was made before it expired."); + delete g_KnifeDecisionTimer; + } - ChangeState(Get5State_GoingLive); - CreateTimer(3.0, StartGoingLive, _, TIMER_FLAG_NO_MAPCHANGE); + EventLogger_LogAndDeleteEvent(knifeEvent); + g_KnifeWinnerTeam = Get5Team_None; + StartGoingLive(); } static bool AwaitingKnifeDecision(int client) { @@ -94,20 +98,20 @@ static bool AwaitingKnifeDecision(int client) { return waiting && (onWinningTeam || admin); } -public Action Command_Stay(int client, int args) { +Action Command_Stay(int client, int args) { if (AwaitingKnifeDecision(client)) { - EndKnifeRound(false); Get5_MessageToAll("%t", "TeamDecidedToStayInfoMessage", g_FormattedTeamNames[g_KnifeWinnerTeam]); + EndKnifeRound(false); } return Plugin_Handled; } -public Action Command_Swap(int client, int args) { +Action Command_Swap(int client, int args) { if (AwaitingKnifeDecision(client)) { - EndKnifeRound(true); Get5_MessageToAll("%t", "TeamDecidedToSwapInfoMessage", g_FormattedTeamNames[g_KnifeWinnerTeam]); + EndKnifeRound(true); } else if (g_GameState == Get5State_Warmup && g_InScrimMode && GetClientMatchTeam(client) == Get5Team_1) { PerformSideSwap(true); @@ -115,7 +119,7 @@ public Action Command_Swap(int client, int args) { return Plugin_Handled; } -public Action Command_Ct(int client, int args) { +Action Command_Ct(int client, int args) { if (IsPlayer(client)) { if (GetClientTeam(client) == CS_TEAM_CT) FakeClientCommand(client, "sm_stay"); @@ -130,7 +134,7 @@ public Action Command_Ct(int client, int args) { return Plugin_Handled; } -public Action Command_T(int client, int args) { +Action Command_T(int client, int args) { if (IsPlayer(client)) { if (GetClientTeam(client) == CS_TEAM_T) FakeClientCommand(client, "sm_stay"); @@ -140,10 +144,11 @@ public Action Command_T(int client, int args) { return Plugin_Handled; } -public Action Timer_ForceKnifeDecision(Handle timer) { +Action Timer_ForceKnifeDecision(Handle timer) { + g_KnifeDecisionTimer = INVALID_HANDLE; if (g_GameState == Get5State_WaitingForKnifeRoundDecision) { - EndKnifeRound(false); Get5_MessageToAll("%t", "TeamLostTimeToDecideInfoMessage", g_FormattedTeamNames[g_KnifeWinnerTeam]); + EndKnifeRound(false); } } diff --git a/scripting/get5/maps.sp b/scripting/get5/maps.sp index efad54842..35ae5240b 100644 --- a/scripting/get5/maps.sp +++ b/scripting/get5/maps.sp @@ -1,5 +1,7 @@ -stock void ChangeMap(const char[] map, float delay = 3.0) { - Get5_MessageToAll("%t", "ChangingMapInfoMessage", map); +void ChangeMap(const char[] map, float delay = 3.0) { + char formattedMapName[64]; + FormatMapName(map, formattedMapName, sizeof(formattedMapName), true, true); + Get5_MessageToAll("%t", "ChangingMapInfoMessage", formattedMapName); // pass the "true" name to a timer to changelevel Handle data = CreateDataPack(); @@ -9,7 +11,7 @@ stock void ChangeMap(const char[] map, float delay = 3.0) { CreateTimer(delay, Timer_DelayedChangeMap, data); } -public Action Timer_DelayedChangeMap(Handle timer, Handle pack) { +static Action Timer_DelayedChangeMap(Handle timer, Handle pack) { char map[PLATFORM_MAX_PATH]; ResetPack(pack); ReadPackString(pack, map, sizeof(map)); diff --git a/scripting/get5/mapveto.sp b/scripting/get5/mapveto.sp index d86800f69..7bbf72ac8 100644 --- a/scripting/get5/mapveto.sp +++ b/scripting/get5/mapveto.sp @@ -4,7 +4,7 @@ #define CONFIRM_NEGATIVE_VALUE "_" -public void CreateVeto() { +void CreateVeto() { if (g_MapPoolList.Length % 2 == 0) { LogError( "Warning, the maplist is even number sized (%d maps), vetos may not function correctly!", @@ -20,8 +20,12 @@ public void CreateVeto() { CreateTimer(1.0, Timer_VetoCountdown, _, TIMER_REPEAT); } -public Action Timer_VetoCountdown(Handle timer) { +static Action Timer_VetoCountdown(Handle timer) { static int warningsPrinted = 0; + if (g_GameState != Get5State_Veto) { + warningsPrinted = 0; + return Plugin_Stop; + } if (warningsPrinted >= g_VetoCountdownCvar.IntValue) { warningsPrinted = 0; Get5Team startingTeam = OtherMatchTeam(g_LastVetoTeam); @@ -30,18 +34,28 @@ public Action Timer_VetoCountdown(Handle timer) { } else { warningsPrinted++; int secondsRemaining = g_VetoCountdownCvar.IntValue - warningsPrinted + 1; - Get5_MessageToAll("%t", "VetoCountdown", secondsRemaining); + char secondsFormatted[32]; + Format(secondsFormatted, sizeof(secondsFormatted), "{GREEN}%d{NORMAL}", secondsRemaining); + Get5_MessageToAll("%t", "VetoCountdown", secondsFormatted); return Plugin_Continue; } } static void AbortVeto() { Get5_MessageToAll("%t", "CaptainLeftOnVetoInfoMessage"); - Get5_MessageToAll("%t", "ReadyToResumeVetoInfoMessage"); + char readyCommandFormatted[64]; + FormatChatCommand(readyCommandFormatted, sizeof(readyCommandFormatted), "!ready"); + Get5_MessageToAll("%t", "ReadyToResumeVetoInfoMessage", readyCommandFormatted); ChangeState(Get5State_PreVeto); + if (g_ActiveVetoMenu != null) { + g_ActiveVetoMenu.Cancel(); + } + if (IsPaused()) { + UnpauseGame(Get5Team_None); + } } -public void VetoFinished() { +static void VetoFinished() { ChangeState(Get5State_Warmup); Get5_MessageToAll("%t", "MapDecidedInfoMessage"); @@ -49,27 +63,38 @@ public void VetoFinished() { UnpauseGame(Get5Team_None); } - // Use total series score as starting point, to not print skipped maps - int seriesScore = g_TeamSeriesScores[Get5Team_1] + g_TeamSeriesScores[Get5Team_2]; - for (int i = seriesScore; i < g_MapsToPlay.Length; i++) { + // If a team has a map advantage, don't print that map. + int mapNumber = Get5_GetMapNumber(); + for (int i = mapNumber; i < g_MapsToPlay.Length; i++) { char map[PLATFORM_MAX_PATH]; g_MapsToPlay.GetString(i, map, sizeof(map)); - Get5_MessageToAll("%t", "MapIsInfoMessage", i + 1 - seriesScore, map); + FormatMapName(map, map, sizeof(map), true, true); + Get5_MessageToAll("%t", "MapIsInfoMessage", i + 1 - mapNumber, map); } + float delay = 10.0; g_MapChangePending = true; - CreateTimer(10.0, Timer_NextMatchMap); + if (!g_SkipVeto && g_DisplayGotvVetoCvar.BoolValue) { + // Players must wait for GOTV to end before we can change map, but we don't need to record that. + CreateTimer(float(GetTvDelay()) + delay, Timer_NextMatchMap); + } else { + CreateTimer(delay, Timer_NextMatchMap); + } + // Always end recording here; ensures that we can successfully start one after veto. + StopRecording(delay); + WriteBackup(); // Write first pre-live backup after veto. } // Main Veto Controller -public void VetoController(int client) { +static void VetoController(int client) { if (!IsPlayer(client) || GetClientMatchTeam(client) == Get5Team_Spec) { AbortVeto(); + return; } int mapsLeft = g_MapsLeftInVetoPool.Length; - int maxMaps = MaxMapsToPlay(g_MapsToWin); + int maxMaps = g_NumberOfMapsInSeries; int mapsPicked = g_MapsToPlay.Length; int sidesSet = g_MapSides.Length; @@ -97,7 +122,7 @@ public void VetoController(int client) { // The purpose is to force the veto process to take a // ban/ban/ban/ban/pick/pick/last map unused process for BO2's. bool bo2_hack = false; - if (g_BO2Match && (mapsLeft == 3 || mapsLeft == 2)) { + if (g_NumberOfMapsInSeries == 2 && (mapsLeft == 3 || mapsLeft == 2)) { bo2_hack = true; } @@ -144,7 +169,7 @@ public void VetoController(int client) { VetoFinished(); } else if (mapsLeft == 1) { - if (g_BO2Match) { + if (g_NumberOfMapsInSeries == 2) { // Terminate the veto since we've had ban-ban-ban-ban-pick-pick VetoFinished(); return; @@ -184,8 +209,8 @@ public void VetoController(int client) { // Confirmations -public void GiveConfirmationMenu(int client, MenuHandler handler, const char[] title, - const char[] confirmChoice) { +static void GiveConfirmationMenu(int client, MenuHandler handler, const char[] title, + const char[] confirmChoice) { // Figure out text for positive and negative values char positiveBuffer[1024], negativeBuffer[1024]; Format(positiveBuffer, sizeof(positiveBuffer), "%T", "ConfirmPositiveOptionText", client); @@ -242,7 +267,7 @@ static bool ConfirmationNegative(const char[] choice) { // Map Vetos -public void GiveMapVetoMenu(int client) { +static void GiveMapVetoMenu(int client) { Menu menu = new Menu(MapVetoMenuHandler); menu.SetTitle("%T", "MapVetoBanMenuText", client); menu.ExitButton = false; @@ -262,8 +287,11 @@ public void GiveMapVetoMenu(int client) { SetConfirmationTime(true); } -public int MapVetoMenuHandler(Menu menu, MenuAction action, int param1, int param2) { +static int MapVetoMenuHandler(Menu menu, MenuAction action, int param1, int param2) { if (action == MenuAction_Select) { + if (g_GameState != Get5State_Veto) { + return; + } int client = param1; Get5Team team = GetClientMatchTeam(client); char mapName[PLATFORM_MAX_PATH]; @@ -282,7 +310,12 @@ public int MapVetoMenuHandler(Menu menu, MenuAction action, int param1, int para RemoveStringFromArray(g_MapsLeftInVetoPool, mapName); - Get5_MessageToAll("%t", "TeamVetoedMapInfoMessage", g_FormattedTeamNames[team], mapName); + char formattedMapName[PLATFORM_MAX_PATH]; + FormatMapName(mapName, formattedMapName, sizeof(formattedMapName), true, false); + // Add color here as FormatMapName would make the color green. + Format(formattedMapName, sizeof(formattedMapName), "{LIGHT_RED}%s{NORMAL}", formattedMapName); + Get5_MessageToAll("%t", "TeamVetoedMapInfoMessage", g_FormattedTeamNames[team], + formattedMapName); Get5MapVetoedEvent event = new Get5MapVetoedEvent(g_MatchID, team, mapName); @@ -300,15 +333,17 @@ public int MapVetoMenuHandler(Menu menu, MenuAction action, int param1, int para if (g_GameState == Get5State_Veto) { AbortVeto(); } - } else if (action == MenuAction_End) { + if (menu == g_ActiveVetoMenu) { + g_ActiveVetoMenu = null; + } delete menu; } } // Map Picks -public void GiveMapPickMenu(int client) { +static void GiveMapPickMenu(int client) { Menu menu = new Menu(MapPickMenuHandler); menu.SetTitle("%T", "MapVetoPickMenuText", client); menu.ExitButton = false; @@ -328,8 +363,11 @@ public void GiveMapPickMenu(int client) { SetConfirmationTime(true); } -public int MapPickMenuHandler(Menu menu, MenuAction action, int param1, int param2) { +static int MapPickMenuHandler(Menu menu, MenuAction action, int param1, int param2) { if (action == MenuAction_Select) { + if (g_GameState != Get5State_Veto) { + return; + } int client = param1; Get5Team team = GetClientMatchTeam(client); char mapName[PLATFORM_MAX_PATH]; @@ -349,8 +387,10 @@ public int MapPickMenuHandler(Menu menu, MenuAction action, int param1, int para g_MapsToPlay.PushString(mapName); RemoveStringFromArray(g_MapsLeftInVetoPool, mapName); - Get5_MessageToAll("%t", "TeamPickedMapInfoMessage", g_FormattedTeamNames[team], mapName, - g_MapsToPlay.Length); + char mapNameFormatted[PLATFORM_MAX_PATH]; + FormatMapName(mapName, mapNameFormatted, sizeof(mapNameFormatted), true, true); + Get5_MessageToAll("%t", "TeamPickedMapInfoMessage", g_FormattedTeamNames[team], + mapNameFormatted, g_MapsToPlay.Length); g_LastVetoTeam = team; Get5MapPickedEvent event = @@ -370,15 +410,17 @@ public int MapPickMenuHandler(Menu menu, MenuAction action, int param1, int para if (g_GameState == Get5State_Veto) { AbortVeto(); } - } else if (action == MenuAction_End) { + if (menu == g_ActiveVetoMenu) { + g_ActiveVetoMenu = null; + } delete menu; } } // Side Picks -public void GiveSidePickMenu(int client) { +static void GiveSidePickMenu(int client) { Menu menu = new Menu(SidePickMenuHandler); menu.ExitButton = false; char mapName[PLATFORM_MAX_PATH]; @@ -391,8 +433,11 @@ public void GiveSidePickMenu(int client) { SetConfirmationTime(true); } -public int SidePickMenuHandler(Menu menu, MenuAction action, int param1, int param2) { +static int SidePickMenuHandler(Menu menu, MenuAction action, int param1, int param2) { if (action == MenuAction_Select) { + if (g_GameState != Get5State_Veto) { + return; + } int client = param1; Get5Team team = GetClientMatchTeam(client); char choice[PLATFORM_MAX_PATH]; @@ -429,6 +474,7 @@ public int SidePickMenuHandler(Menu menu, MenuAction action, int param1, int par char mapName[PLATFORM_MAX_PATH]; g_MapsToPlay.GetString(mapNumber, mapName, sizeof(mapName)); + Format(choice, sizeof(choice), "{GREEN}%s{NORMAL}", choice); Get5_MessageToAll("%t", "TeamSelectSideInfoMessage", g_FormattedTeamNames[team], choice, mapName); @@ -449,8 +495,10 @@ public int SidePickMenuHandler(Menu menu, MenuAction action, int param1, int par if (g_GameState == Get5State_Veto) { AbortVeto(); } - } else if (action == MenuAction_End) { + if (menu == g_ActiveVetoMenu) { + g_ActiveVetoMenu = null; + } delete menu; } } diff --git a/scripting/get5/matchconfig.sp b/scripting/get5/matchconfig.sp index 6c9f3294c..4d428c851 100644 --- a/scripting/get5/matchconfig.sp +++ b/scripting/get5/matchconfig.sp @@ -10,10 +10,11 @@ #define CONFIG_SPECTATORSNAME_DEFAULT "casters" #define CONFIG_NUM_MAPSDEFAULT 3 #define CONFIG_SKIPVETO_DEFAULT false +#define CONFIG_CLINCH_SERIES_DEFAULT true #define CONFIG_VETOFIRST_DEFAULT "team1" #define CONFIG_SIDETYPE_DEFAULT "standard" -stock bool LoadMatchConfig(const char[] config, bool restoreBackup = false) { +bool LoadMatchConfig(const char[] config, bool restoreBackup = false) { if (g_GameState != Get5State_None && !restoreBackup) { return false; } @@ -35,10 +36,13 @@ stock bool LoadMatchConfig(const char[] config, bool restoreBackup = false) { ClearArray(GetTeamAuths(team)); } + g_MatchID = ""; g_ReadyTimeWaitingUsed = 0; - g_ForceWinnerSignal = false; - g_ForcedWinner = Get5Team_None; - + g_HasKnifeRoundStarted = false; + g_MapChangePending = false; + g_MapNumber = 0; + g_NumberOfMapsInSeries = 0; + g_RoundNumber = -1; g_LastVetoTeam = Get5Team_2; g_MapPoolList.Clear(); g_MapsLeftInVetoPool.Clear(); @@ -58,12 +62,6 @@ stock bool LoadMatchConfig(const char[] config, bool restoreBackup = false) { return false; } - if (!g_CheckAuthsCvar.BoolValue && - (GetTeamAuths(Get5Team_1).Length != 0 || GetTeamAuths(Get5Team_2).Length != 0)) { - LogError( - "Setting player auths in the \"players\" section has no impact with get5_check_auths 0"); - } - // Copy all the maps into the veto pool. char mapName[PLATFORM_MAX_PATH]; for (int i = 0; i < g_MapPoolList.Length; i++) { @@ -74,52 +72,64 @@ stock bool LoadMatchConfig(const char[] config, bool restoreBackup = false) { g_TeamScoresPerMap.Set(g_TeamScoresPerMap.Length - 1, 0, 1); } - if (g_BO2Match) { - g_MapsToWin = 2; - } - - if (MaxMapsToPlay(g_MapsToWin) > g_MapPoolList.Length) { + if (g_NumberOfMapsInSeries > g_MapPoolList.Length) { MatchConfigFail("Cannot play a series of %d maps with a maplist of %d maps", - MaxMapsToPlay(g_MapsToWin), g_MapPoolList.Length); + g_NumberOfMapsInSeries, g_MapPoolList.Length); return false; } if (g_SkipVeto) { // Copy the first k maps from the maplist to the final match maps. - for (int i = 0; i < MaxMapsToPlay(g_MapsToWin); i++) { + for (int i = 0; i < g_NumberOfMapsInSeries; i++) { g_MapPoolList.GetString(i, mapName, sizeof(mapName)); g_MapsToPlay.PushString(mapName); // Push a map side if one hasn't been set yet. if (g_MapSides.Length < g_MapsToPlay.Length) { - if (g_MatchSideType == MatchSideType_Standard) { - g_MapSides.Push(SideChoice_KnifeRound); - } else if (g_MatchSideType == MatchSideType_AlwaysKnife) { + if (g_MatchSideType == MatchSideType_Standard || + g_MatchSideType == MatchSideType_AlwaysKnife) { g_MapSides.Push(SideChoice_KnifeRound); - } else if (g_MatchSideType == MatchSideType_NeverKnife) { + } else { g_MapSides.Push(SideChoice_Team1CT); } } } - g_MapPoolList.GetString(Get5_GetMapNumber(), mapName, sizeof(mapName)); - ChangeState(Get5State_Warmup); - - char currentMap[PLATFORM_MAX_PATH]; - GetCurrentMap(currentMap, sizeof(currentMap)); - if (!StrEqual(mapName, currentMap) && !restoreBackup) { - ChangeMap(mapName); + if (!restoreBackup) { + ChangeState(Get5State_Warmup); + // When restoring from backup, changelevel is called after loading the match config. + g_MapPoolList.GetString(Get5_GetMapNumber(), mapName, sizeof(mapName)); + char currentMap[PLATFORM_MAX_PATH]; + GetCurrentMap(currentMap, sizeof(currentMap)); + if (!StrEqual(mapName, currentMap)) { + ChangeMap(mapName); + } } - } else { + } else if (!restoreBackup) { ChangeState(Get5State_PreVeto); } + if (g_GameState == Get5State_None) { + // Make sure here that we don't run the code below in game state none, but also not overriding + // PreVeto. Currently, this could happen if you restored a backup with skip_veto:false. + ChangeState(Get5State_Warmup); + } + + // Before we run the Get5_OnSeriesInit forward, we want to ensure that as much game state is set + // as possible, so that any implementation reacting to that event/forward will have all the + // natives return proper data. ExecuteMatchConfigCvars gets called twice because + // ExecCfg(g_WarmupCfgCvar) also does it async, but we need it here as the team assigment below + // depends on it. We set this one first as the others may depend on something changed in the match + // cvars section. + ExecuteMatchConfigCvars(); + SetMatchTeamCvars(); + LoadPlayerNames(); + AddTeamLogosToDownloadTable(); + SetStartingTeams(); + if (!restoreBackup) { - SetStartingTeams(); ExecCfg(g_WarmupCfgCvar); - ExecuteMatchConfigCvars(); - LoadPlayerNames(); - EnsureIndefiniteWarmup(); + StartWarmup(); if (IsPaused()) { LogDebug("Match was paused when loading match config. Unpausing."); UnpauseGame(Get5Team_None); @@ -137,29 +147,49 @@ stock bool LoadMatchConfig(const char[] config, bool restoreBackup = false) { Call_Finish(); EventLogger_LogAndDeleteEvent(startEvent); - } - for (int i = 1; i <= MaxClients; i++) { - if (IsAuthedPlayer(i)) { - if (GetClientMatchTeam(i) == Get5Team_None) { - RememberAndKickClient(i, "%t", "YouAreNotAPlayerInfoMessage"); - } else { - CheckClientTeam(i); + if (!g_CheckAuthsCvar.BoolValue && + (GetTeamAuths(Get5Team_1).Length != 0 || GetTeamAuths(Get5Team_2).Length != 0 || + GetTeamCoaches(Get5Team_1).Length != 0 || GetTeamCoaches(Get5Team_2).Length != 0)) { + LogError( + "Setting player auths in the \"players\" or \"coaches\" section has no impact with get5_check_auths 0"); + } + + // ExecuteMatchConfigCvars must be executed before we place players, as it might have + // get5_check_auths 1. We must also have called SetStartingTeams to get the sides right. When + // restoring from backup, assigning to teams is done after loading the match config as it + // depends on the sides being set correctly by the backup, so we put it inside this "if" here. + // When the match is loaded, we do not want to assign players on no team, as they may be in the + // process of joining the server, which is the reason for the timer callback. This has caused + // problems with players getting stuck on no team when using match config autoload, essentially + // recreating the "coaching bug". Adding a second seems to solve this problem. We cannot just + // skip team none, as players may also just be on the team selection menu when the match is + // loaded, meaning they will never have a joingame hook, as it already happened, and we still + // want those players placed. + LOOP_CLIENTS(i) { + if (IsPlayer(i)) { + if (GetClientTeam(i) == CS_TEAM_NONE) { + CreateTimer(1.0, Timer_PlacePlayerFromTeamNone, i, TIMER_FLAG_NO_MAPCHANGE); + } else { + CheckClientTeam(i); + } } } } - AddTeamLogosToDownloadTable(); - SetMatchTeamCvars(); - ExecuteMatchConfigCvars(); - LoadPlayerNames(); strcopy(g_LoadedConfigFile, sizeof(g_LoadedConfigFile), config); Get5_MessageToAll("%t", "MatchConfigLoadedInfoMessage"); return true; } -public bool LoadMatchFile(const char[] config) { +static Action Timer_PlacePlayerFromTeamNone(Handle timer, int client) { + if (g_GameState != Get5State_None && IsPlayer(client)) { + CheckClientTeam(client); + } +} + +static bool LoadMatchFile(const char[] config) { Get5PreloadMatchConfigEvent event = new Get5PreloadMatchConfigEvent(config); LogDebug("Calling Get5_OnPreLoadMatchConfig()"); @@ -270,8 +300,8 @@ stock bool LoadMatchFromUrl(const char[] url, ArrayList paramNames = null, } // SteamWorks HTTP callback for fetching a workshop collection -public int SteamWorks_OnMatchConfigReceived(Handle request, bool failure, bool requestSuccessful, - EHTTPStatusCode statusCode, Handle data) { +static int SteamWorks_OnMatchConfigReceived(Handle request, bool failure, bool requestSuccessful, + EHTTPStatusCode statusCode, Handle data) { if (failure || !requestSuccessful) { MatchConfigFail("Steamworks GET request failed, HTTP status code = %d", statusCode); return; @@ -285,17 +315,17 @@ public int SteamWorks_OnMatchConfigReceived(Handle request, bool failure, bool r strcopy(g_LoadedConfigFile, sizeof(g_LoadedConfigFile), g_LoadedConfigUrl); } -public void WriteMatchToKv(KeyValues kv) { +void WriteMatchToKv(KeyValues kv) { kv.SetString("matchid", g_MatchID); kv.SetNum("scrim", g_InScrimMode); - kv.SetNum("maps_to_win", g_MapsToWin); - kv.SetNum("bo2_series", g_BO2Match); kv.SetNum("skip_veto", g_SkipVeto); + kv.SetNum("num_maps", g_NumberOfMapsInSeries); kv.SetNum("players_per_team", g_PlayersPerTeam); kv.SetNum("coaches_per_team", g_CoachesPerTeam); kv.SetNum("min_players_to_ready", g_MinPlayersToReady); kv.SetNum("min_spectators_to_ready", g_MinSpectatorsToReady); kv.SetString("match_title", g_MatchTitle); + kv.SetNum("clinch_series", g_SeriesCanClinch); kv.SetNum("favored_percentage_team1", g_FavoredTeamPercentage); kv.SetString("favored_percentage_text", g_FavoredTeamText); @@ -371,32 +401,19 @@ static bool LoadMatchFromKv(KeyValues kv) { g_InScrimMode = kv.GetNum("scrim") != 0; kv.GetString("match_title", g_MatchTitle, sizeof(g_MatchTitle), CONFIG_MATCHTITLE_DEFAULT); g_PlayersPerTeam = kv.GetNum("players_per_team", CONFIG_PLAYERSPERTEAM_DEFAULT); + g_SeriesCanClinch = kv.GetNum("clinch_series", CONFIG_CLINCH_SERIES_DEFAULT) != 0; g_CoachesPerTeam = kv.GetNum("coaches_per_team", CONFIG_COACHESPERTEAM_DEFAULT); g_MinPlayersToReady = kv.GetNum("min_players_to_ready", CONFIG_MINPLAYERSTOREADY_DEFAULT); g_MinSpectatorsToReady = kv.GetNum("min_spectators_to_ready", CONFIG_MINSPECTATORSTOREADY_DEFAULT); g_SkipVeto = kv.GetNum("skip_veto", CONFIG_SKIPVETO_DEFAULT) != 0; - // bo2_series and maps_to_win are deprecated. They are used if provided, but otherwise - // num_maps' default is the fallback. - bool bo2 = (kv.GetNum("bo2_series", false) != 0); - int mapsToWin = kv.GetNum("maps_to_win", 0); - int numMaps = kv.GetNum("num_maps", CONFIG_NUM_MAPSDEFAULT); - if (bo2 || numMaps == 2) { - g_BO2Match = true; - g_MapsToWin = 2; - } else { - g_BO2Match = false; - if (mapsToWin >= 1) { - g_MapsToWin = mapsToWin; - } else { - // Normal path. No even numbers allowed since we already handled bo2. - if (numMaps % 2 == 0) { - MatchConfigFail("Cannot create a series of %d maps. Use a odd number or 2.", numMaps); - return false; - } - g_MapsToWin = (numMaps + 1) / 2; - } + g_NumberOfMapsInSeries = kv.GetNum("num_maps", CONFIG_NUM_MAPSDEFAULT); + g_MapsToWin = MapsToWin(g_NumberOfMapsInSeries); + if (g_NumberOfMapsInSeries != 2 && g_NumberOfMapsInSeries % 2 == 0) { + MatchConfigFail("Cannot create a series of %d maps. Use an odd number or 2.", + g_NumberOfMapsInSeries); + return false; } char vetoFirstBuffer[64]; @@ -412,13 +429,11 @@ static bool LoadMatchFromKv(KeyValues kv) { GetTeamAuths(Get5Team_Spec).Clear(); if (kv.JumpToKey("spectators")) { - AddSubsectionAuthsToList(kv, "players", GetTeamAuths(Get5Team_Spec), AUTH_LENGTH); + AddSubsectionAuthsToList(kv, "players", GetTeamAuths(Get5Team_Spec)); kv.GetString("name", g_TeamNames[Get5Team_Spec], MAX_CVAR_LENGTH, CONFIG_SPECTATORSNAME_DEFAULT); kv.GoBack(); - - Format(g_FormattedTeamNames[Get5Team_Spec], MAX_CVAR_LENGTH, "%s%s{NORMAL}", - g_DefaultTeamColors[Get5Team_Spec], g_TeamNames[Get5Team_Spec]); + FormatTeamName(Get5Team_Spec); } if (kv.JumpToKey("team1")) { @@ -462,7 +477,7 @@ static bool LoadMatchFromKv(KeyValues kv) { char value[MAX_CVAR_LENGTH]; do { kv.GetSectionName(name, sizeof(name)); - kv.GetString(NULL_STRING, value, sizeof(value)); + ReadEmptyStringInsteadOfPlaceholder(kv, value, sizeof(value)); g_CvarNames.PushString(name); g_CvarValues.PushString(value); } while (kv.GotoNextKey(false)); @@ -478,6 +493,7 @@ static bool LoadMatchFromJson(JSON_Object json) { json_object_get_string_safe(json, "matchid", g_MatchID, sizeof(g_MatchID), CONFIG_MATCHID_DEFAULT); g_InScrimMode = json_object_get_bool_safe(json, "scrim", false); + g_SeriesCanClinch = json_object_get_bool_safe(json, "clinch_series", true); json_object_get_string_safe(json, "match_title", g_MatchTitle, sizeof(g_MatchTitle), CONFIG_MATCHTITLE_DEFAULT); @@ -491,27 +507,12 @@ static bool LoadMatchFromJson(JSON_Object json) { CONFIG_MINSPECTATORSTOREADY_DEFAULT); g_SkipVeto = json_object_get_bool_safe(json, "skip_veto", CONFIG_SKIPVETO_DEFAULT); - // bo2_series and maps_to_win are deprecated. They are used if provided, but otherwise - // num_maps' default is the fallback. - bool bo2 = json_object_get_bool_safe(json, "bo2_series", false); - int mapsToWin = json_object_get_int_safe(json, "maps_to_win", 0); - int numMaps = json_object_get_int_safe(json, "num_maps", CONFIG_NUM_MAPSDEFAULT); - - if (bo2 || numMaps == 2) { - g_BO2Match = true; - g_MapsToWin = 2; - } else { - g_BO2Match = false; - if (mapsToWin >= 1) { - g_MapsToWin = mapsToWin; - } else { - // Normal path. No even numbers allowed since we already handled bo2. - if (numMaps % 2 == 0) { - MatchConfigFail("Cannot create a series of %d maps. Use a odd number or 2.", numMaps); - return false; - } - g_MapsToWin = (numMaps + 1) / 2; - } + g_NumberOfMapsInSeries = json_object_get_int_safe(json, "num_maps", CONFIG_NUM_MAPSDEFAULT); + g_MapsToWin = MapsToWin(g_NumberOfMapsInSeries); + if (g_NumberOfMapsInSeries != 2 && g_NumberOfMapsInSeries % 2 == 0) { + MatchConfigFail("Cannot create a series of %d maps. Use an odd number or 2.", + g_NumberOfMapsInSeries); + return false; } char vetoFirstBuffer[64]; @@ -533,9 +534,7 @@ static bool LoadMatchFromJson(JSON_Object json) { json_object_get_string_safe(spec, "name", g_TeamNames[Get5Team_Spec], MAX_CVAR_LENGTH, CONFIG_SPECTATORSNAME_DEFAULT); AddJsonAuthsToList(spec, "players", GetTeamAuths(Get5Team_Spec), AUTH_LENGTH); - - Format(g_FormattedTeamNames[Get5Team_Spec], MAX_CVAR_LENGTH, "%s%s{NORMAL}", - g_DefaultTeamColors[Get5Team_Spec], g_TeamNames[Get5Team_Spec]); + FormatTeamName(Get5Team_Spec); } JSON_Object team1 = json.GetObject("team1"); @@ -603,8 +602,8 @@ static void LoadTeamDataJson(JSON_Object json, Get5Team matchTeam) { if (StrEqual(fromfile, "")) { // TODO: this needs to support both an array and a dictionary // For now, it only supports an array - JSON_Object coaches = json.GetObject("coaches"); AddJsonAuthsToList(json, "players", GetTeamAuths(matchTeam), AUTH_LENGTH); + JSON_Object coaches = json.GetObject("coaches"); if (coaches != null) { AddJsonAuthsToList(json, "coaches", GetTeamCoaches(matchTeam), AUTH_LENGTH); } @@ -619,14 +618,12 @@ static void LoadTeamDataJson(JSON_Object json, Get5Team matchTeam) { LogError("Cannot load team config from file \"%s\", fromfile"); } else { LoadTeamDataJson(fromfileJson, matchTeam); - fromfileJson.Cleanup(); - delete fromfileJson; + json_cleanup_and_delete(fromfileJson); } } g_TeamSeriesScores[matchTeam] = json_object_get_int_safe(json, "series_score", 0); - Format(g_FormattedTeamNames[matchTeam], MAX_CVAR_LENGTH, "%s%s{NORMAL}", - g_DefaultTeamColors[matchTeam], g_TeamNames[matchTeam]); + FormatTeamName(matchTeam); } static void LoadTeamData(KeyValues kv, Get5Team matchTeam) { @@ -635,8 +632,8 @@ static void LoadTeamData(KeyValues kv, Get5Team matchTeam) { kv.GetString("fromfile", fromfile, sizeof(fromfile)); if (StrEqual(fromfile, "")) { - AddSubsectionAuthsToList(kv, "players", GetTeamAuths(matchTeam), AUTH_LENGTH); - AddSubsectionAuthsToList(kv, "coaches", GetTeamCoaches(matchTeam), AUTH_LENGTH); + AddSubsectionAuthsToList(kv, "players", GetTeamAuths(matchTeam)); + AddSubsectionAuthsToList(kv, "coaches", GetTeamCoaches(matchTeam)); kv.GetString("name", g_TeamNames[matchTeam], MAX_CVAR_LENGTH, ""); kv.GetString("tag", g_TeamTags[matchTeam], MAX_CVAR_LENGTH, ""); kv.GetString("flag", g_TeamFlags[matchTeam], MAX_CVAR_LENGTH, ""); @@ -653,8 +650,21 @@ static void LoadTeamData(KeyValues kv, Get5Team matchTeam) { } g_TeamSeriesScores[matchTeam] = kv.GetNum("series_score", 0); - Format(g_FormattedTeamNames[matchTeam], MAX_CVAR_LENGTH, "%s%s{NORMAL}", - g_DefaultTeamColors[matchTeam], g_TeamNames[matchTeam]); + FormatTeamName(matchTeam); +} + +static void FormatTeamName(const Get5Team team) { + char color[32]; + if (team == Get5Team_1) { + g_Team1NameColorCvar.GetString(color, sizeof(color)); + } else if (team == Get5Team_2) { + g_Team2NameColorCvar.GetString(color, sizeof(color)); + } else if (team == Get5Team_Spec) { + g_SpecNameColorCvar.GetString(color, sizeof(color)); + } else { + color = "{NORMAL}"; + } + Format(g_FormattedTeamNames[team], MAX_CVAR_LENGTH, "%s%s{NORMAL}", color, g_TeamNames[team]); } static void LoadDefaultMapList(ArrayList list) { @@ -677,7 +687,7 @@ static void LoadDefaultMapList(ArrayList list) { } } -public void SetMatchTeamCvars() { +void SetMatchTeamCvars() { Get5Team ctTeam = Get5Team_1; Get5Team tTeam = Get5Team_2; if (g_TeamStartingSide[Get5Team_1] == CS_TEAM_T) { @@ -685,8 +695,6 @@ public void SetMatchTeamCvars() { tTeam = Get5Team_1; } - int mapsPlayed = Get5_GetMapNumber(); - // Get the match configs set by the config file. // These might be modified so copies are made here. char ctMatchText[MAX_CVAR_LENGTH]; @@ -697,8 +705,8 @@ public void SetMatchTeamCvars() { // Update mp_teammatchstat_txt with the match title. char mapstat[MAX_CVAR_LENGTH]; strcopy(mapstat, sizeof(mapstat), g_MatchTitle); - ReplaceStringWithInt(mapstat, sizeof(mapstat), "{MAPNUMBER}", mapsPlayed + 1); - ReplaceStringWithInt(mapstat, sizeof(mapstat), "{MAXMAPS}", MaxMapsToPlay(g_MapsToWin)); + ReplaceStringWithInt(mapstat, sizeof(mapstat), "{MAPNUMBER}", Get5_GetMapNumber() + 1, false); + ReplaceStringWithInt(mapstat, sizeof(mapstat), "{MAXMAPS}", g_NumberOfMapsInSeries, false); SetConVarStringSafe("mp_teammatchstat_txt", mapstat); if (g_MapsToWin >= 3) { @@ -736,17 +744,7 @@ public void SetMatchTeamCvars() { } } -public Get5Team GetMapWinner(int mapNumber) { - int team1score = GetMapScore(mapNumber, Get5Team_1); - int team2score = GetMapScore(mapNumber, Get5Team_2); - if (team1score > team2score) { - return Get5Team_1; - } else { - return Get5Team_2; - } -} - -public void ExecuteMatchConfigCvars() { +static void ExecuteMatchConfigCvars() { // Save the original match cvar values if we haven't already. if (g_MatchConfigChangedCvars == INVALID_HANDLE) { g_MatchConfigChangedCvars = SaveCvars(g_CvarNames); @@ -766,7 +764,7 @@ public void ExecuteMatchConfigCvars() { } } -public Action Command_LoadTeam(int client, int args) { +Action Command_LoadTeam(int client, int args) { if (g_GameState == Get5State_None) { ReplyToCommand(client, "Cannot change player lists when there is no match to modify"); return Plugin_Handled; @@ -804,15 +802,21 @@ public Action Command_LoadTeam(int client, int args) { return Plugin_Handled; } -public Action Command_AddPlayer(int client, int args) { +Action Command_AddPlayer(int client, int args) { if (g_GameState == Get5State_None) { - ReplyToCommand(client, "Cannot change player lists when there is no match to modify"); + ReplyToCommand(client, "No match configuration was loaded."); return Plugin_Handled; - } - - if (g_InScrimMode) { + } else if (g_InScrimMode) { ReplyToCommand( - client, "Cannot use get5_addplayer in scrim mode. Use get5_ringer to swap a players team."); + client, + "Cannot use get5_addplayer in scrim mode. Use get5_ringer to swap a player's team."); + return Plugin_Handled; + } else if (g_DoingBackupRestoreNow || g_WaitingForRoundBackup) { + ReplyToCommand(client, "Cannot add players while waiting for round backup."); + return Plugin_Handled; + } else if (g_PendingSideSwap || InHalftimePhase()) { + ReplyToCommand(client, + "Cannot add players during halftime. Please wait until the next round starts."); return Plugin_Handled; } @@ -840,7 +844,10 @@ public Action Command_AddPlayer(int client, int args) { if (AddPlayerToTeam(auth, team, name)) { ReplyToCommand(client, "Successfully added player %s to %s.", auth, teamString); } else { - ReplyToCommand(client, "Failed to add player %s to team %. They may already be on a team or you provided an invalid Steam ID.", auth, teamString); + ReplyToCommand( + client, + "Failed to add player %s to team %. They may already be on a team or you provided an invalid Steam ID.", + auth, teamString); } } else { @@ -849,12 +856,23 @@ public Action Command_AddPlayer(int client, int args) { return Plugin_Handled; } -public Action Command_AddCoach(int client, int args) { +Action Command_AddCoach(int client, int args) { if (g_GameState == Get5State_None) { - ReplyToCommand(client, "Cannot change coach targets when there is no match to modify"); + ReplyToCommand(client, "No match configuration was loaded."); return Plugin_Handled; } else if (!g_CoachingEnabledCvar.BoolValue) { - ReplyToCommand(client, "Cannot change coach targets if coaching is disabled."); + ReplyToCommand(client, "Coaching is not enabled."); + return Plugin_Handled; + } else if (g_InScrimMode) { + ReplyToCommand(client, + "Coaches cannot be added in scrim mode. Use the !coach command in chat."); + return Plugin_Handled; + } else if (g_DoingBackupRestoreNow || g_WaitingForRoundBackup) { + ReplyToCommand(client, "Cannot add coaches while waiting for round backup."); + return Plugin_Handled; + } else if (g_PendingSideSwap || InHalftimePhase()) { + ReplyToCommand(client, + "Cannot add coaches during halftime. Please wait until the next round starts."); return Plugin_Handled; } @@ -877,23 +895,37 @@ public Action Command_AddCoach(int client, int args) { return Plugin_Handled; } - if (GetTeamCoaches(team).Length == g_CoachesPerTeam) { + if (CountCoachesOnTeam(team) == g_CoachesPerTeam) { ReplyToCommand(client, "Coach Spots are full for %s.", teamString); return Plugin_Handled; } if (AddCoachToTeam(auth, team, name)) { - // Check if we are in the playerlist already and remove. + // If the player is already on the team as a regular player, remove them when adding to + // coaches. int index = GetTeamAuths(team).FindString(auth); if (index >= 0) { GetTeamAuths(team).Erase(index); } - // Update the backup structure as well for round restores, covers edge - // case of users joining, coaching, stopping, and getting 16k cash as player. - WriteBackup(); + ReplyToCommand(client, "Successfully added player %s as coach for %s.", auth, teamString); + + // If the user is already on the server as a player, move them to coaching immediately. + int addedClient = AuthToClient(auth); + if (addedClient > 0 && IsClientConnected(addedClient)) { + Get5Side side = view_as(Get5TeamToCSTeam(team)); + if (side != Get5Side_None) { + LogDebug( + "Player %s was present on the server when added as coach; moving them to coach for %d.", + auth, team); + SetClientCoaching(addedClient, side); + } + } } else { - ReplyToCommand(client, "Failed to add player %s as coach for %s. They may already be coaching or you provided an invalid Steam ID.", auth, teamString); + ReplyToCommand( + client, + "Failed to add player %s as coach for %s. They may already be coaching or you provided an invalid Steam ID.", + auth, teamString); } } else { ReplyToCommand(client, "Usage: get5_addcoach [name]"); @@ -901,16 +933,21 @@ public Action Command_AddCoach(int client, int args) { return Plugin_Handled; } -public Action Command_AddKickedPlayer(int client, int args) { +Action Command_AddKickedPlayer(int client, int args) { if (g_GameState == Get5State_None) { - ReplyToCommand(client, "Cannot change player lists when there is no match to modify"); + ReplyToCommand(client, "No match configuration was loaded."); return Plugin_Handled; - } - - if (g_InScrimMode) { + } else if (g_InScrimMode) { ReplyToCommand( client, - "Cannot use get5_addkickedplayer in scrim mode. Use get5_ringer to swap a players team."); + "Cannot use get5_addkickedplayer in scrim mode. Use get5_ringer to swap a player's team."); + return Plugin_Handled; + } else if (g_DoingBackupRestoreNow || g_WaitingForRoundBackup) { + ReplyToCommand(client, "Cannot add players while waiting for round backup."); + return Plugin_Handled; + } else if (g_PendingSideSwap || InHalftimePhase()) { + ReplyToCommand(client, + "Cannot add players during halftime. Please wait until the next round starts."); return Plugin_Handled; } @@ -939,10 +976,13 @@ public Action Command_AddKickedPlayer(int client, int args) { } if (AddPlayerToTeam(g_LastKickedPlayerAuth, team, name)) { - ReplyToCommand(client, "Successfully added kicked player %s to %s.", - g_LastKickedPlayerAuth, teamString); + ReplyToCommand(client, "Successfully added kicked player %s to %s.", g_LastKickedPlayerAuth, + teamString); } else { - ReplyToCommand(client, "Failed to add player %s to %s. They may already be on a team or you provided an invalid Steam ID.", g_LastKickedPlayerAuth, teamString); + ReplyToCommand( + client, + "Failed to add player %s to %s. They may already be on a team or you provided an invalid Steam ID.", + g_LastKickedPlayerAuth, teamString); } } else { @@ -951,7 +991,7 @@ public Action Command_AddKickedPlayer(int client, int args) { return Plugin_Handled; } -public Action Command_RemovePlayer(int client, int args) { +Action Command_RemovePlayer(int client, int args) { if (g_GameState == Get5State_None) { ReplyToCommand(client, "Cannot change player lists when there is no match to modify"); return Plugin_Handled; @@ -969,7 +1009,8 @@ public Action Command_RemovePlayer(int client, int args) { if (RemovePlayerFromTeams(auth)) { ReplyToCommand(client, "Successfully removed player %s.", auth); } else { - ReplyToCommand(client, "Player %s not found in auth lists or the Steam ID was invalid.", auth); + ReplyToCommand(client, "Player %s not found in auth lists or the Steam ID was invalid.", + auth); } } else { ReplyToCommand(client, "Usage: get5_removeplayer "); @@ -977,7 +1018,7 @@ public Action Command_RemovePlayer(int client, int args) { return Plugin_Handled; } -public Action Command_RemoveKickedPlayer(int client, int args) { +Action Command_RemoveKickedPlayer(int client, int args) { if (g_GameState == Get5State_None) { ReplyToCommand(client, "Cannot change player lists when there is no match to modify."); return Plugin_Handled; @@ -998,12 +1039,13 @@ public Action Command_RemoveKickedPlayer(int client, int args) { if (RemovePlayerFromTeams(g_LastKickedPlayerAuth)) { ReplyToCommand(client, "Successfully removed kicked player %s.", g_LastKickedPlayerAuth); } else { - ReplyToCommand(client, "Player %s not found in auth lists or the Steam ID was invalid.", g_LastKickedPlayerAuth); + ReplyToCommand(client, "Player %s not found in auth lists or the Steam ID was invalid.", + g_LastKickedPlayerAuth); } return Plugin_Handled; } -public Action Command_CreateMatch(int client, int args) { +Action Command_CreateMatch(int client, int args) { if (g_GameState != Get5State_None) { ReplyToCommand(client, "Cannot create a match when a match is already loaded"); return Plugin_Handled; @@ -1014,15 +1056,15 @@ public Action Command_CreateMatch(int client, int args) { GetCleanMapName(matchMap, sizeof(matchMap)); if (args >= 1) { - GetCmdArg(1, matchid, sizeof(matchid)); - } - if (args >= 2) { - GetCmdArg(2, matchMap, sizeof(matchMap)); + GetCmdArg(1, matchMap, sizeof(matchMap)); if (!IsMapValid(matchMap)) { ReplyToCommand(client, "Invalid map: %s", matchMap); return Plugin_Handled; } } + if (args >= 2) { + GetCmdArg(2, matchid, sizeof(matchid)); + } char path[PLATFORM_MAX_PATH]; Format(path, sizeof(path), "get5_%s.cfg", matchid); @@ -1030,9 +1072,10 @@ public Action Command_CreateMatch(int client, int args) { KeyValues kv = new KeyValues("Match"); kv.SetString("matchid", matchid); - kv.SetNum("maps_to_win", 1); + kv.SetNum("num_maps", 1); kv.SetNum("skip_veto", 1); kv.SetNum("players_per_team", 5); + kv.SetNum("clinch_series", 1); kv.JumpToKey("maplist", true); kv.SetString(matchMap, KEYVALUE_STRING_PLACEHOLDER); @@ -1040,16 +1083,19 @@ public Action Command_CreateMatch(int client, int args) { char teamName[MAX_CVAR_LENGTH]; + // If team names are empty because nobody is on on the server, the will be set by + // CheckTeamNameStatus during ready-phase. We cannot write empty strings to KeyValues, so we just + // skip them. kv.JumpToKey("team1", true); - int count = AddPlayersToAuthKv(kv, Get5Team_1, teamName); - if (count > 0) + if (AddPlayersToAuthKv(kv, Get5Team_1, teamName) > 0) { kv.SetString("name", teamName); + } kv.GoBack(); kv.JumpToKey("team2", true); - count = AddPlayersToAuthKv(kv, Get5Team_2, teamName); - if (count > 0) + if (AddPlayersToAuthKv(kv, Get5Team_2, teamName) > 0) { kv.SetString("name", teamName); + } kv.GoBack(); kv.JumpToKey("spectators", true); @@ -1067,13 +1113,13 @@ public Action Command_CreateMatch(int client, int args) { return Plugin_Handled; } -public Action Command_CreateScrim(int client, int args) { +Action Command_CreateScrim(int client, int args) { if (g_GameState != Get5State_None) { ReplyToCommand(client, "Cannot create a match when a match is already loaded"); return Plugin_Handled; } - char matchid[MATCH_ID_LENGTH] = ""; + char matchid[MATCH_ID_LENGTH] = "scrim"; char matchMap[PLATFORM_MAX_PATH]; GetCleanMapName(matchMap, sizeof(matchMap)); char otherTeamName[MAX_CVAR_LENGTH] = "Away"; @@ -1110,24 +1156,12 @@ public Action Command_CreateScrim(int client, int args) { MatchConfigFail("Failed to read scrim template in %s", templateFile); return Plugin_Handled; } - + // Because we read the field and write it again, then load it as a match config, we have to make + // sure empty strings are not being skipped. if (kv.JumpToKey("team1") && kv.JumpToKey("players") && kv.GotoFirstSubKey(false)) { - // Empty string values are found when reading KeyValues, but don't get written out. - // So this adds a value for each auth so scrim templates don't have to insert fake values. + char name[MAX_NAME_LENGTH]; do { - char auth[AUTH_LENGTH]; - char name[MAX_NAME_LENGTH]; - kv.GetString(NULL_STRING, name, sizeof(name), KEYVALUE_STRING_PLACEHOLDER); - kv.GetSectionName(auth, sizeof(auth)); - - // This shouldn't be necessary, but when the name field was empty, the - // use of KEYVALUE_STRING_PLACEHOLDER as a default doesn't seem to work. - // TODO: figure out what's going on with needing this here. - if (StrEqual(name, "")) { - name = KEYVALUE_STRING_PLACEHOLDER; - } - - kv.SetString(NULL_STRING, name); + WritePlaceholderInsteadOfEmptyString(kv, name, sizeof(name)); } while (kv.GotoNextKey(false)); kv.Rewind(); } else { @@ -1136,6 +1170,18 @@ public Action Command_CreateScrim(int client, int args) { return Plugin_Handled; } + // Also ensure empty string values in cvars get printed to the match config. + if (kv.JumpToKey("cvars")) { + if (kv.GotoFirstSubKey(false)) { + char cVarValue[MAX_CVAR_LENGTH]; + do { + WritePlaceholderInsteadOfEmptyString(kv, cVarValue, sizeof(cVarValue)); + } while (kv.GotoNextKey(false)); + kv.GoBack(); + } + kv.GoBack(); + } + kv.JumpToKey("team2", true); kv.SetString("name", otherTeamName); kv.GoBack(); @@ -1151,9 +1197,9 @@ public Action Command_CreateScrim(int client, int args) { return Plugin_Handled; } -public Action Command_Ringer(int client, int args) { +Action Command_Ringer(int client, int args) { if (g_GameState == Get5State_None || !g_InScrimMode) { - ReplyToCommand(client, "This command can only be used in scrim mode"); + ReplyToCommand(client, "This command can only be used in scrim mode."); return Plugin_Handled; } @@ -1177,7 +1223,7 @@ static int AddPlayersToAuthKv(KeyValues kv, Get5Team team, char teamName[MAX_CVA kv.JumpToKey("players", true); bool gotClientName = false; char auth[AUTH_LENGTH]; - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (IsAuthedPlayer(i)) { int csTeam = GetClientTeam(i); Get5Team t = Get5Team_None; @@ -1230,15 +1276,26 @@ static void AddTeamLogoToDownloadTable(const char[] logoName) { return; char logoPath[PLATFORM_MAX_PATH + 1]; - Format(logoPath, sizeof(logoPath), "resource/flash/econ/tournaments/teams/%s.png", logoName); - - LogDebug("Adding file %s to download table", logoName); - AddFileToDownloadsTable(logoPath); + Format(logoPath, sizeof(logoPath), "materials/panorama/images/tournaments/teams/%s.svg", + logoName); + if (FileExists(logoPath)) { + LogDebug("Adding file %s to download table", logoName); + AddFileToDownloadsTable(logoPath); + } else { + Format(logoPath, sizeof(logoPath), "resource/flash/econ/tournaments/teams/%s.png", logoName); + if (FileExists(logoPath)) { + LogDebug("Adding file %s to download table", logoName); + AddFileToDownloadsTable(logoPath); + } else { + LogError("Error in locating file %s. Please ensure the file exists on your game server.", + logoPath); + } + } } -public void CheckTeamNameStatus(Get5Team team) { +void CheckTeamNameStatus(Get5Team team) { if (StrEqual(g_TeamNames[team], "") && team != Get5Team_Spec) { - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (IsAuthedPlayer(i)) { if (GetClientMatchTeam(i) == team) { char clientName[MAX_NAME_LENGTH]; @@ -1248,12 +1305,21 @@ public void CheckTeamNameStatus(Get5Team team) { } } } + FormatTeamName(team); + } +} - char colorTag[32] = TEAM1_COLOR; - if (team == Get5Team_2) - colorTag = TEAM2_COLOR; +void ExecCfg(ConVar cvar) { + char cfg[PLATFORM_MAX_PATH]; + cvar.GetString(cfg, sizeof(cfg)); + ServerCommand("exec \"%s\"", cfg); + CreateTimer(0.1, Timer_ExecMatchConfig, _, TIMER_FLAG_NO_MAPCHANGE); +} - Format(g_FormattedTeamNames[team], MAX_CVAR_LENGTH, "%s%s{NORMAL}", colorTag, - g_TeamNames[team]); - } +static Action Timer_ExecMatchConfig(Handle timer) { + // When we load config files using ServerCommand("exec") above, which is async, we want match + // config cvars to always override. + ExecuteMatchConfigCvars(); + SetMatchTeamCvars(); + return Plugin_Handled; } diff --git a/scripting/get5/natives.sp b/scripting/get5/natives.sp index 683a29f2f..aebb20bdd 100644 --- a/scripting/get5/natives.sp +++ b/scripting/get5/natives.sp @@ -31,7 +31,7 @@ public int Native_GetGameState(Handle plugin, int numParams) { public int Native_Message(Handle plugin, int numParams) { int client = GetNativeCell(1); - if (client != 0 && (!IsClientConnected(client) || !IsClientInGame(client))) + if (client != 0 && !IsClientInGame(client)) return; char buffer[1024]; @@ -49,9 +49,9 @@ public int Native_Message(Handle plugin, int numParams) { Format(finalMsg, sizeof(finalMsg), "%s %s", prefix, buffer); if (client == 0) { - Colorize(finalMsg, sizeof(finalMsg), false); + Colorize(finalMsg, sizeof(finalMsg), true); PrintToConsole(client, finalMsg); - } else if (IsClientInGame(client)) { + } else { Colorize(finalMsg, sizeof(finalMsg)); PrintToChat(client, finalMsg); } @@ -65,7 +65,7 @@ public int Native_MessageToTeam(Handle plugin, int numParams) { char buffer[1024]; int bytesWritten = 0; - for (int i = 0; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (!IsPlayer(i) || GetClientMatchTeam(i) != team) { continue; } @@ -90,9 +90,11 @@ public int Native_MessageToAll(Handle plugin, int numParams) { char buffer[1024]; int bytesWritten = 0; + // Don't use LOOP_CLIENTS(i) because we need client 0 here. for (int i = 0; i <= MaxClients; i++) { - if (i != 0 && (!IsClientConnected(i) || !IsClientInGame(i))) + if (i != 0 && !IsClientInGame(i)) { continue; + } SetGlobalTransTarget(i); FormatNativeString(0, 1, 2, sizeof(buffer), bytesWritten, buffer); @@ -103,12 +105,12 @@ public int Native_MessageToAll(Handle plugin, int numParams) { else Format(finalMsg, sizeof(finalMsg), "%s %s", prefix, buffer); - if (i != 0) { + if (i == 0) { + Colorize(finalMsg, sizeof(finalMsg), true); + PrintToConsole(i, finalMsg); + } else { Colorize(finalMsg, sizeof(finalMsg)); PrintToChat(i, finalMsg); - } else { - Colorize(finalMsg, sizeof(finalMsg), false); - PrintToConsole(i, finalMsg); } } } @@ -143,11 +145,13 @@ public int Native_SetPlayerName(Handle plugin, int numParams) { char name[MAX_NAME_LENGTH]; GetNativeString(1, auth, sizeof(auth)); GetNativeString(2, name, sizeof(name)); + bool suppressPlayerNameLoad = GetNativeCell(3); char steam64[AUTH_LENGTH]; - ConvertAuthToSteam64(auth, steam64); - if (strlen(name) > 0 && !StrEqual(name, KEYVALUE_STRING_PLACEHOLDER)) { + if (strlen(name) > 0 && ConvertAuthToSteam64(auth, steam64)) { g_PlayerNames.SetString(steam64, name); - LoadPlayerNames(); + if (!suppressPlayerNameLoad) { + LoadPlayerNames(); + } } } diff --git a/scripting/get5/pausing.sp b/scripting/get5/pausing.sp index 88a17d029..c9ef6cf85 100644 --- a/scripting/get5/pausing.sp +++ b/scripting/get5/pausing.sp @@ -1,11 +1,10 @@ -public bool PauseableGameState() { +static bool PauseableGameState() { return (g_GameState == Get5State_KnifeRound || - g_GameState == Get5State_WaitingForKnifeRoundDecision || - g_GameState == Get5State_Live || - g_GameState == Get5State_GoingLive); + g_GameState == Get5State_WaitingForKnifeRoundDecision || g_GameState == Get5State_Live || + g_GameState == Get5State_GoingLive); } -public void PauseGame(Get5Team team, Get5PauseType type) { +void PauseGame(Get5Team team, Get5PauseType type) { if (type == Get5PauseType_None) { LogError("PauseGame() called with Get5PauseType_None. Please call UnpauseGame() instead."); UnpauseGame(team); @@ -37,12 +36,13 @@ public void PauseGame(Get5Team team, Get5PauseType type) { CreateTimer(0.1, Timer_ResetPauseRestriction); } -public Action Timer_ResetPauseRestriction(Handle timer, int data) { +static Action Timer_ResetPauseRestriction(Handle timer, int data) { g_IsChangingPauseState = false; } -stock void UnpauseGame(Get5Team team) { - Get5MatchUnpausedEvent event = new Get5MatchUnpausedEvent(g_MatchID, g_MapNumber, team, g_PauseType); +void UnpauseGame(Get5Team team) { + Get5MatchUnpausedEvent event = + new Get5MatchUnpausedEvent(g_MatchID, g_MapNumber, team, g_PauseType); LogDebug("Calling Get5_OnMatchUnpaused()"); @@ -60,15 +60,16 @@ stock void UnpauseGame(Get5Team team) { CreateTimer(0.1, Timer_ResetPauseRestriction); } -public Action Command_PauseOrUnpauseMatch(int client, const char[] command, int argc) { +Action Command_PauseOrUnpauseMatch(int client, const char[] command, int argc) { if (g_GameState == Get5State_None || g_IsChangingPauseState) { return Plugin_Continue; } - ReplyToCommand(client, "Get5 prevents calls to %s. Administrators should use sm_pause/sm_unpause.", command); + ReplyToCommand( + client, "Get5 prevents calls to %s. Administrators should use sm_pause/sm_unpause.", command); return Plugin_Stop; } -public Action Command_TechPause(int client, int args) { +Action Command_TechPause(int client, int args) { if (client == 0) { // Redirect admin use of sm_tech to regular pause. We only have one type of admin pause. return Command_Pause(client, args); @@ -95,7 +96,8 @@ public Action Command_TechPause(int client, int args) { if (g_PauseType != Get5PauseType_None) { g_TeamReadyForUnpause[team] = false; - LogDebug("Ignoring technical pause request as game is already paused; setting team to not ready to unpause."); + LogDebug( + "Ignoring technical pause request as game is already paused; setting team to not ready to unpause."); return Plugin_Handled; } @@ -121,14 +123,17 @@ public Action Command_TechPause(int client, int args) { g_TechnicalPausesUsed[team]++; PauseGame(team, Get5PauseType_Tech); - Get5_MessageToAll("%t", "MatchTechPausedByTeamMessage", client); + char formattedClientName[MAX_NAME_LENGTH]; + FormatPlayerName(formattedClientName, sizeof(formattedClientName), client, team); + Get5_MessageToAll("%t", "MatchTechPausedByTeamMessage", formattedClientName); if (maxTechPauses > 0) { - Get5_MessageToAll("%t", "TechPausePausesRemaining", g_FormattedTeamNames[team], maxTechPauses - g_TechnicalPausesUsed[team]); + Get5_MessageToAll("%t", "TechPausePausesRemaining", g_FormattedTeamNames[team], + maxTechPauses - g_TechnicalPausesUsed[team]); } return Plugin_Handled; } -public Action Command_Pause(int client, int args) { +Action Command_Pause(int client, int args) { if (client == 0) { PauseGame(Get5Team_None, Get5PauseType_Admin); Get5_MessageToAll("%t", "AdminForcePauseInfoMessage"); @@ -151,7 +156,8 @@ public Action Command_Pause(int client, int args) { if (g_PauseType != Get5PauseType_None) { g_TeamReadyForUnpause[team] = false; - LogDebug("Ignoring tactical pause request as game is already paused; setting team to not ready to unpause."); + LogDebug( + "Ignoring tactical pause request as game is already paused; setting team to not ready to unpause."); return Plugin_Handled; } @@ -161,8 +167,10 @@ public Action Command_Pause(int client, int args) { int maxPauseTime = g_MaxPauseTimeCvar.IntValue; if (maxPauseTime > 0 && g_TacticalPauseTimeUsed[team] >= maxPauseTime) { char maxPauseTimeFormatted[16]; - convertSecondsToMinutesAndSeconds(maxPauseTime, maxPauseTimeFormatted, sizeof(maxPauseTimeFormatted)); - Get5_Message(client, "%t", "MaxPausesTimeUsedInfoMessage", maxPauseTimeFormatted, g_FormattedTeamNames[team]); + convertSecondsToMinutesAndSeconds(maxPauseTime, maxPauseTimeFormatted, + sizeof(maxPauseTimeFormatted)); + Get5_Message(client, "%t", "MaxPausesTimeUsedInfoMessage", maxPauseTimeFormatted, + g_FormattedTeamNames[team]); return Plugin_Handled; } } @@ -176,7 +184,9 @@ public Action Command_Pause(int client, int args) { PauseGame(team, Get5PauseType_Tactical); if (IsPlayer(client)) { - Get5_MessageToAll("%t", "MatchPausedByTeamMessage", client); + char formattedClientName[MAX_NAME_LENGTH]; + FormatPlayerName(formattedClientName, sizeof(formattedClientName), client, team); + Get5_MessageToAll("%t", "MatchPausedByTeamMessage", formattedClientName); } if (maxPauses > 0) { @@ -188,7 +198,7 @@ public Action Command_Pause(int client, int args) { return Plugin_Handled; } -public Action Command_Unpause(int client, int args) { +Action Command_Unpause(int client, int args) { if (!IsPaused()) { // Game is not paused; ignore command. return Plugin_Handled; @@ -221,33 +231,38 @@ public Action Command_Unpause(int client, int args) { int techPausesUsed = g_TechnicalPausesUsed[g_PausingTeam]; if ((maxTechPauseDuration > 0 && g_LatestPauseDuration >= maxTechPauseDuration) || - (maxTechPauses > 0 && techPausesUsed > maxTechPauses) - ) { + (maxTechPauses > 0 && techPausesUsed > maxTechPauses)) { UnpauseGame(team); if (IsPlayer(client)) { - Get5_MessageToAll("%t", "MatchUnpauseInfoMessage", client); + char formattedClientName[MAX_NAME_LENGTH]; + FormatPlayerName(formattedClientName, sizeof(formattedClientName), client, team); + Get5_MessageToAll("%t", "MatchUnpauseInfoMessage", formattedClientName); } return Plugin_Handled; } } + char formattedUnpauseCommand[64]; + FormatChatCommand(formattedUnpauseCommand, sizeof(formattedUnpauseCommand), "!unpause"); if (g_TeamReadyForUnpause[Get5Team_1] && g_TeamReadyForUnpause[Get5Team_2]) { UnpauseGame(team); if (IsPlayer(client)) { - Get5_MessageToAll("%t", "MatchUnpauseInfoMessage", client); + char formattedClientName[MAX_NAME_LENGTH]; + FormatPlayerName(formattedClientName, sizeof(formattedClientName), client, team); + Get5_MessageToAll("%t", "MatchUnpauseInfoMessage", formattedClientName); } } else if (!g_TeamReadyForUnpause[Get5Team_2]) { Get5_MessageToAll("%t", "WaitingForUnpauseInfoMessage", g_FormattedTeamNames[Get5Team_1], - g_FormattedTeamNames[Get5Team_2]); + g_FormattedTeamNames[Get5Team_2], formattedUnpauseCommand); } else if (!g_TeamReadyForUnpause[Get5Team_1]) { Get5_MessageToAll("%t", "WaitingForUnpauseInfoMessage", g_FormattedTeamNames[Get5Team_2], - g_FormattedTeamNames[Get5Team_1]); + g_FormattedTeamNames[Get5Team_1], formattedUnpauseCommand); } return Plugin_Handled; } -public Action Timer_PauseTimeCheck(Handle timer) { +static Action Timer_PauseTimeCheck(Handle timer) { if (g_PauseType == Get5PauseType_None || !IsPaused()) { LogDebug("Stopping pause timer as game is not paused."); return Plugin_Stop; @@ -269,14 +284,13 @@ public Action Timer_PauseTimeCheck(Handle timer) { CSTeamString(Get5TeamToCSTeam(team), teamString, sizeof(teamString)); if (g_PauseType == Get5PauseType_Tactical) { - int maxTacticalPauseTime = g_MaxPauseTimeCvar.IntValue; int maxTacticalPauses = g_MaxTacticalPausesCvar.IntValue; int tacticalPausesUsed = g_TacticalPausesUsed[team]; int fixedPauseTime = g_FixedPauseTimeCvar.IntValue; if (fixedPauseTime > 0 && fixedPauseTime < 15) { - fixedPauseTime = 15; // Don't allow less than 15 second fixed pauses. + fixedPauseTime = 15; // Don't allow less than 15 second fixed pauses. } // -1 assumes unlimited. @@ -289,23 +303,27 @@ public Action Timer_PauseTimeCheck(Handle timer) { return Plugin_Stop; } } else if (maxTacticalPauses > 0 && tacticalPausesUsed > maxTacticalPauses) { - // The game gets unpaused if the number of maximum pauses changes to below the number of used pauses while a - // pause is active. Kind of a weird edge-case, but it should be handled gracefully. - Get5_MessageToAll("%t", "MaxPausesUsedInfoMessage", maxTacticalPauses, g_FormattedTeamNames[g_PausingTeam]); + // The game gets unpaused if the number of maximum pauses changes to below the number of used + // pauses while a pause is active. Kind of a weird edge-case, but it should be handled + // gracefully. + Get5_MessageToAll("%t", "MaxPausesUsedInfoMessage", maxTacticalPauses, + g_FormattedTeamNames[g_PausingTeam]); UnpauseGame(team); return Plugin_Stop; } else if (!g_TeamReadyForUnpause[team]) { - // If the team that called the pause has indicated they are ready, no more time should be subtracted from their - // maximum pause time, but the timer must keep running as they could go back to not-ready-for-unpause before the - // other team unpauses, in which case we would keep counting their seconds used. + // If the team that called the pause has indicated they are ready, no more time should be + // subtracted from their maximum pause time, but the timer must keep running as they could go + // back to not-ready-for-unpause before the other team unpauses, in which case we would keep + // counting their seconds used. g_TacticalPauseTimeUsed[team]++; - LogDebug("Adding tactical pause time used for Get5Team %d. Now: %d", team, g_TacticalPauseTimeUsed[team]); + LogDebug("Adding tactical pause time used for Get5Team %d. Now: %d", team, + g_TacticalPauseTimeUsed[team]); if (maxTacticalPauseTime > 0) { timeLeft = maxTacticalPauseTime - g_TacticalPauseTimeUsed[team]; if (timeLeft <= 0) { - Get5_MessageToAll("%t", "PauseRunoutInfoMessage", g_FormattedTeamNames[team]); - UnpauseGame(team); - return Plugin_Stop; + Get5_MessageToAll("%t", "PauseRunoutInfoMessage", g_FormattedTeamNames[team]); + UnpauseGame(team); + return Plugin_Stop; } } } @@ -318,45 +336,56 @@ public Action Timer_PauseTimeCheck(Handle timer) { char pauseTimeMaxFormatted[16] = ""; if (timeLeft >= 0) { - convertSecondsToMinutesAndSeconds(maxTacticalPauseTime, pauseTimeMaxFormatted, sizeof(pauseTimeMaxFormatted)); + convertSecondsToMinutesAndSeconds(maxTacticalPauseTime, pauseTimeMaxFormatted, + sizeof(pauseTimeMaxFormatted)); } LOOP_CLIENTS(i) { - if (IsPlayer(i)) { - if (fixedPauseTime) { // If fixed pause; takes precedence over total time and reuses timeLeft for simplicity + if (IsValidClient(i)) { + if (fixedPauseTime) { // If fixed pause; takes precedence over total time and reuses + // timeLeft for simplicity if (maxTacticalPauses > 0) { // Team A (CT) tactical pause (2/4): 0:45 - PrintHintText(i, "%s (%s) %t (%d/%d): %s", g_TeamNames[team], teamString, "TacticalPauseMidSentence", tacticalPausesUsed, maxTacticalPauses, timeLeftFormatted); + PrintHintText(i, "%s (%s) %t (%d/%d): %s", g_TeamNames[team], teamString, + "TacticalPauseMidSentence", tacticalPausesUsed, maxTacticalPauses, + timeLeftFormatted); } else { // Team A (CT) tactical pause: 0:45 - PrintHintText(i, "%s (%s) %t: %s", g_TeamNames[team], teamString, "TacticalPauseMidSentence", timeLeftFormatted); + PrintHintText(i, "%s (%s) %t: %s", g_TeamNames[team], teamString, + "TacticalPauseMidSentence", timeLeftFormatted); } - } else if (timeLeft >= 0) { // If total time restriction + } else if (timeLeft >= 0) { // If total time restriction if (maxTacticalPauses > 0) { // Team A (CT) tactical pause (2/4). // Remaining pause time: 0:45 / 3:00 - PrintHintText(i, "%s (%s) %t (%d/%d).\n%t: %s / %s", g_TeamNames[team], teamString, "TacticalPauseMidSentence", tacticalPausesUsed, maxTacticalPauses, "PauseTimeRemainingPrefix", timeLeftFormatted, pauseTimeMaxFormatted); + PrintHintText(i, "%s (%s) %t (%d/%d).\n%t: %s / %s", g_TeamNames[team], teamString, + "TacticalPauseMidSentence", tacticalPausesUsed, maxTacticalPauses, + "PauseTimeRemainingPrefix", timeLeftFormatted, pauseTimeMaxFormatted); } else { // Team A (CT) tactical pause. // Remaining pause time: 0:45 / 3:00 - PrintHintText(i, "%s (%s) %t.\n%t: %s / %s", g_TeamNames[team], teamString, "TacticalPauseMidSentence", "PauseTimeRemainingPrefix", timeLeftFormatted, pauseTimeMaxFormatted); + PrintHintText(i, "%s (%s) %t.\n%t: %s / %s", g_TeamNames[team], teamString, + "TacticalPauseMidSentence", "PauseTimeRemainingPrefix", timeLeftFormatted, + pauseTimeMaxFormatted); } - } else { // if no time restriction or awaiting unpause + } else { // if no time restriction or awaiting unpause if (maxTacticalPauses > 0) { // Team A (CT) tactical pause (2/4). // Awaiting unpause. - PrintHintText(i, "%s (%s) %t (%d/%d).\n%t.", g_TeamNames[team], teamString, "TacticalPauseMidSentence", tacticalPausesUsed, maxTacticalPauses, "AwaitingUnpause"); + PrintHintText(i, "%s (%s) %t (%d/%d).\n%t.", g_TeamNames[team], teamString, + "TacticalPauseMidSentence", tacticalPausesUsed, maxTacticalPauses, + "AwaitingUnpause"); } else { // Team A (CT) tactical pause. // Awaiting unpause. - PrintHintText(i, "%s (%s) %t.\n%t.", g_TeamNames[team], teamString, "TacticalPauseMidSentence", "AwaitingUnpause"); + PrintHintText(i, "%s (%s) %t.\n%t.", g_TeamNames[team], teamString, + "TacticalPauseMidSentence", "AwaitingUnpause"); } } } } } else if (g_PauseType == Get5PauseType_Tech) { - int maxTechPauseDuration = g_MaxTechPauseDurationCvar.IntValue; int maxTechPauses = g_MaxTechPausesCvar.IntValue; int techPausesUsed = g_TechnicalPausesUsed[team]; @@ -364,15 +393,19 @@ public Action Timer_PauseTimeCheck(Handle timer) { // -1 assumes unlimited. int timeLeft = -1; - // If tech pause max is reduced to below what is used, we don't want to print remaining time, as anyone can unpause. - // We achieve this by simply skipping the time calculation if max tech pauses have been exceeded. + // If tech pause max is reduced to below what is used, we don't want to print remaining time, as + // anyone can unpause. We achieve this by simply skipping the time calculation if max tech + // pauses have been exceeded. if (!g_TeamReadyForUnpause[team] && (maxTechPauses == 0 || techPausesUsed <= maxTechPauses)) { if (maxTechPauseDuration > 0) { timeLeft = maxTechPauseDuration - g_LatestPauseDuration; if (timeLeft == 0) { - // Only print to chat when hitting 0, but keep the timer going as tech pauses don't unpause on their own. - // The PrintHintText below will inform users that they can now unpause. - Get5_MessageToAll("%t", "TechPauseRunoutInfoMessage"); + // Only print to chat when hitting 0, but keep the timer going as tech pauses don't + // unpause on their own. The PrintHintText below will inform users that they can now + // unpause. + char formattedUnpauseCommand[64]; + FormatChatCommand(formattedUnpauseCommand, sizeof(formattedUnpauseCommand), "!unpause"); + Get5_MessageToAll("%t", "TechPauseRunoutInfoMessage", formattedUnpauseCommand); } } } @@ -384,43 +417,47 @@ public Action Timer_PauseTimeCheck(Handle timer) { } LOOP_CLIENTS(i) { - if (IsPlayer(i)) { + if (IsValidClient(i)) { if (timeLeft >= 0) { if (maxTechPauses > 0) { // Team A (CT) technical pause (3/4): Time remaining before anyone can unpause: 1:30 - PrintHintText(i, "%s (%s) %t (%d/%d).\n%t: %s", g_TeamNames[team], teamString, "TechnicalPauseMidSentence", techPausesUsed, maxTechPauses, "TimeRemainingBeforeAnyoneCanUnpausePrefix", timeLeftFormatted); + PrintHintText(i, "%s (%s) %t (%d/%d).\n%t: %s", g_TeamNames[team], teamString, + "TechnicalPauseMidSentence", techPausesUsed, maxTechPauses, + "TimeRemainingBeforeAnyoneCanUnpausePrefix", timeLeftFormatted); } else { // Team A (CT) technical pause. Time remaining before anyone can unpause: 1:30 - PrintHintText(i, "%s (%s) %t.\n%t: %s", g_TeamNames[team], teamString, "TechnicalPauseMidSentence", "TimeRemainingBeforeAnyoneCanUnpausePrefix", timeLeftFormatted); + PrintHintText(i, "%s (%s) %t.\n%t: %s", g_TeamNames[team], teamString, + "TechnicalPauseMidSentence", "TimeRemainingBeforeAnyoneCanUnpausePrefix", + timeLeftFormatted); } } else { if (maxTechPauses > 0) { // Team A (CT) technical pause (3/4). Awaiting unpause. - PrintHintText(i, "%s (%s) %t (%d/%d).\n%t.", g_TeamNames[team], teamString, "TechnicalPauseMidSentence", techPausesUsed, maxTechPauses, "AwaitingUnpause"); + PrintHintText(i, "%s (%s) %t (%d/%d).\n%t.", g_TeamNames[team], teamString, + "TechnicalPauseMidSentence", techPausesUsed, maxTechPauses, + "AwaitingUnpause"); } else { // Team A (CT) technical pause. Awaiting unpause. - PrintHintText(i, "%s (%s) %t.\n%t.", g_TeamNames[team], teamString, "TechnicalPauseMidSentence", "AwaitingUnpause"); + PrintHintText(i, "%s (%s) %t.\n%t.", g_TeamNames[team], teamString, + "TechnicalPauseMidSentence", "AwaitingUnpause"); } } } } } else if (g_PauseType == Get5PauseType_Admin) { - LOOP_CLIENTS(i) { - if (IsPlayer(i)) { + if (IsValidClient(i)) { PrintHintText(i, "%t", "PausedByAdministrator"); } } } else if (g_PauseType == Get5PauseType_Backup) { - LOOP_CLIENTS(i) { - if (IsPlayer(i)) { + if (IsValidClient(i)) { PrintHintText(i, "%t", "PausedForBackup"); } } - } return Plugin_Continue; } diff --git a/scripting/get5/readysystem.sp b/scripting/get5/readysystem.sp index 0d1786e76..4afb3ffbf 100644 --- a/scripting/get5/readysystem.sp +++ b/scripting/get5/readysystem.sp @@ -2,26 +2,27 @@ * Ready System */ -public void ResetReadyStatus() { +void ResetReadyStatus() { SetAllTeamsForcedReady(false); SetAllClientsReady(false); } -public bool IsReadyGameState() { - return g_GameState == Get5State_PreVeto || g_GameState == Get5State_Warmup; +static bool IsReadyGameState() { + return (g_GameState == Get5State_PreVeto || g_GameState == Get5State_Warmup) && + !g_MapChangePending; } // Client ready status -public bool IsClientReady(int client) { +static bool IsClientReady(int client) { return g_ClientReady[client] == true; } -public void SetClientReady(int client, bool ready) { +void SetClientReady(int client, bool ready) { g_ClientReady[client] = ready; } -public void SetAllClientsReady(bool ready) { +static void SetAllClientsReady(bool ready) { LOOP_CLIENTS(i) { SetClientReady(i, ready); } @@ -29,15 +30,15 @@ public void SetAllClientsReady(bool ready) { // Team ready override -public bool IsTeamForcedReady(Get5Team team) { +static bool IsTeamForcedReady(Get5Team team) { return g_TeamReadyOverride[team] == true; } -public void SetTeamForcedReady(Get5Team team, bool ready) { +static void SetTeamForcedReady(Get5Team team, bool ready) { g_TeamReadyOverride[team] = ready; } -public void SetAllTeamsForcedReady(bool ready) { +static void SetAllTeamsForcedReady(bool ready) { LOOP_TEAMS(team) { SetTeamForcedReady(team, ready); } @@ -45,15 +46,15 @@ public void SetAllTeamsForcedReady(bool ready) { // Team ready status -public bool IsTeamsReady() { +bool IsTeamsReady() { return IsTeamReady(Get5Team_1) && IsTeamReady(Get5Team_2); } -public bool IsSpectatorsReady() { +bool IsSpectatorsReady() { return IsTeamReady(Get5Team_Spec); } -public bool IsTeamReady(Get5Team team) { +bool IsTeamReady(Get5Team team) { if (g_GameState == Get5State_Live) { return true; } @@ -82,7 +83,7 @@ public bool IsTeamReady(Get5Team team) { return false; } -public int GetTeamReadyCount(Get5Team team) { +static int GetTeamReadyCount(Get5Team team) { int readyCount = 0; LOOP_CLIENTS(i) { if (IsPlayer(i) && GetClientMatchTeam(i) == team && !IsClientCoaching(i) && IsClientReady(i)) { @@ -92,7 +93,7 @@ public int GetTeamReadyCount(Get5Team team) { return readyCount; } -public int GetTeamPlayerCount(Get5Team team) { +static int GetTeamPlayerCount(Get5Team team) { int playerCount = 0; LOOP_CLIENTS(i) { if (IsPlayer(i) && GetClientMatchTeam(i) == team && !IsClientCoaching(i)) { @@ -102,7 +103,7 @@ public int GetTeamPlayerCount(Get5Team team) { return playerCount; } -public int GetTeamMinReady(Get5Team team) { +static int GetTeamMinReady(Get5Team team) { if (team == Get5Team_1 || team == Get5Team_2) { return g_MinPlayersToReady; } else if (team == Get5Team_Spec) { @@ -112,7 +113,7 @@ public int GetTeamMinReady(Get5Team team) { } } -public int GetPlayersPerTeam(Get5Team team) { +static int GetPlayersPerTeam(Get5Team team) { if (team == Get5Team_1 || team == Get5Team_2) { return g_PlayersPerTeam; } else if (team == Get5Team_Spec) { @@ -125,7 +126,7 @@ public int GetPlayersPerTeam(Get5Team team) { // Admin commands -public Action Command_AdminForceReady(int client, int args) { +Action Command_AdminForceReady(int client, int args) { if (!IsReadyGameState()) { return Plugin_Handled; } @@ -140,7 +141,7 @@ public Action Command_AdminForceReady(int client, int args) { // Client commands // Re-used to automatically ready players on warmup-activity, hence the helper-method. -public void HandleReadyCommand(int client, bool autoReady) { +void HandleReadyCommand(int client, bool autoReady) { if (!IsReadyGameState()) { return; } @@ -153,7 +154,8 @@ public void HandleReadyCommand(int client, bool autoReady) { Get5_Message(client, "%t", "YouAreReady"); if (autoReady) { - PrintHintText(client, "%t", "YouAreReadyAuto"); + // We cannot color text in hints, so no formatting the command. + PrintHintText(client, "%t", "YouAreReadyAuto", "!unready"); } SetClientReady(client, true); @@ -163,12 +165,12 @@ public void HandleReadyCommand(int client, bool autoReady) { } } -public Action Command_Ready(int client, int args) { +Action Command_Ready(int client, int args) { HandleReadyCommand(client, false); return Plugin_Handled; } -public Action Command_NotReady(int client, int args) { +Action Command_NotReady(int client, int args) { Get5Team team = GetClientMatchTeam(client); if (!IsReadyGameState() || team == Get5Team_None || !IsClientReady(client)) { return Plugin_Handled; @@ -196,7 +198,7 @@ public Action Command_NotReady(int client, int args) { return Plugin_Handled; } -public Action Command_ForceReadyClient(int client, int args) { +Action Command_ForceReadyClient(int client, int args) { Get5Team team = GetClientMatchTeam(client); if (!IsReadyGameState() || team == Get5Team_None || IsTeamReady(team)) { return Plugin_Handled; @@ -209,11 +211,12 @@ public Action Command_ForceReadyClient(int client, int args) { Get5_Message(client, "%t", "TeamFailToReadyMinPlayerCheck", minReady); return Plugin_Handled; } - + char formattedClientName[MAX_NAME_LENGTH]; + FormatPlayerName(formattedClientName, sizeof(formattedClientName), client, team); LOOP_CLIENTS(i) { if (IsPlayer(i) && GetClientMatchTeam(i) == team) { SetClientReady(i, true); - Get5_Message(i, "%t", "TeammateForceReadied", client); + Get5_Message(i, "%t", "TeammateForceReadied", formattedClientName); } } SetTeamForcedReady(team, true); @@ -242,10 +245,9 @@ static void HandleReadyMessage(Get5Team team) { if (g_GameState == Get5State_PreVeto) { Get5_MessageToAll("%t", "TeamReadyToVetoInfoMessage", g_FormattedTeamNames[team]); } else if (g_GameState == Get5State_Warmup) { - SideChoice sides = view_as(g_MapSides.Get(Get5_GetMapNumber())); if (g_WaitingForRoundBackup) { Get5_MessageToAll("%t", "TeamReadyToRestoreBackupInfoMessage", g_FormattedTeamNames[team]); - } else if (sides == SideChoice_KnifeRound) { + } else if (view_as(g_MapSides.Get(g_MapNumber)) == SideChoice_KnifeRound) { Get5_MessageToAll("%t", "TeamReadyToKnifeInfoMessage", g_FormattedTeamNames[team]); } else { Get5_MessageToAll("%t", "TeamReadyToBeginInfoMessage", g_FormattedTeamNames[team]); @@ -253,13 +255,13 @@ static void HandleReadyMessage(Get5Team team) { } } -public void MissingPlayerInfoMessage() { +void MissingPlayerInfoMessage() { MissingPlayerInfoMessageTeam(Get5Team_1); MissingPlayerInfoMessageTeam(Get5Team_2); MissingPlayerInfoMessageTeam(Get5Team_Spec); } -public void MissingPlayerInfoMessageTeam(Get5Team team) { +static void MissingPlayerInfoMessageTeam(Get5Team team) { if (IsTeamForcedReady(team)) { return; } @@ -269,14 +271,20 @@ public void MissingPlayerInfoMessageTeam(Get5Team team) { int playerCount = GetTeamPlayerCount(team); int readyCount = GetTeamReadyCount(team); - if (playerCount == readyCount && playerCount < minPlayers && readyCount >= minReady) { - Get5_MessageToTeam(team, "%t", "ForceReadyInfoMessage", minPlayers); + if (playerCount == readyCount && playerCount < minPlayers && readyCount >= minReady && + minPlayers > 1) { + char minPlayersFormatted[32]; + Format(minPlayersFormatted, sizeof(minPlayersFormatted), "{GREEN}%d{NORMAL}", minPlayers); + char forceReadyFormatted[64]; + FormatChatCommand(forceReadyFormatted, sizeof(forceReadyFormatted), "!forceready"); + Get5_MessageToTeam(team, "%t", "ForceReadyInfoMessage", forceReadyFormatted, + minPlayersFormatted); } } // Helpers -public void UpdateClanTags() { +void UpdateClanTags() { if (!g_SetClientClanTagCvar.BoolValue) { LogDebug("Not setting client clan tags because get5_set_client_clan_tags is 0"); return; diff --git a/scripting/get5/recording.sp b/scripting/get5/recording.sp new file mode 100644 index 000000000..9e873f8b7 --- /dev/null +++ b/scripting/get5/recording.sp @@ -0,0 +1,139 @@ +bool StartRecording() { + char demoFormat[PLATFORM_MAX_PATH]; + g_DemoNameFormatCvar.GetString(demoFormat, sizeof(demoFormat)); + if (StrEqual("", demoFormat)) { + LogMessage("Demo recording is disabled via get5_demo_name_format."); + return false; + } + + if (!IsTVEnabled()) { + LogError( + "Demo recording will not work with \"tv_enable 0\". Set \"tv_enable 1\" and restart the map to fix this."); + g_DemoFileName = ""; + return false; + } + + char demoName[PLATFORM_MAX_PATH + 1]; + if (!FormatCvarString(g_DemoNameFormatCvar, demoName, sizeof(demoName))) { + LogError("Failed to format demo filename. Please check your demo file format convar."); + g_DemoFileName = ""; + return false; + } + + Format(g_DemoFileName, sizeof(g_DemoFileName), "%s.dem", demoName); + LogMessage("Recording to %s", g_DemoFileName); + + // Escape unsafe characters and start recording. .dem is appended to the filename automatically. + char szDemoName[PLATFORM_MAX_PATH + 1]; + strcopy(szDemoName, sizeof(szDemoName), demoName); + ReplaceString(szDemoName, sizeof(szDemoName), "\"", "\\\""); + ServerCommand("tv_record \"%s\"", szDemoName); + Stats_SetDemoName(g_DemoFileName); + return true; +} + +void StopRecording(float delay = 0.0) { + if (delay < 0.1) { + LogDebug("Stopping GOTV recording immediately."); + StopRecordingCallback(g_MatchID, g_MapNumber, g_DemoFileName); + } else { + LogDebug("Starting timer that will end GOTV recording in %f seconds.", delay); + CreateTimer(delay, Timer_StopGoTVRecording, + GetDemoInfoDataPack(g_MatchID, g_MapNumber, g_DemoFileName)); + } + g_DemoFileName = ""; +} + +static void StopRecordingCallback(const char[] matchId, const int mapNumber, + const char[] demoFileName) { + ServerCommand("tv_stoprecord"); + if (StrEqual("", demoFileName)) { + LogDebug("Demo was not recorded by Get5; not firing Get5_OnDemoFinished()"); + return; + } + // We delay this by 3 seconds to allow the server to flush to the file before firing the event. + CreateTimer(3.0, Timer_FireStopRecordingEvent, + GetDemoInfoDataPack(matchId, mapNumber, demoFileName)); +} + +static DataPack GetDemoInfoDataPack(const char[] matchId, const int mapNumber, + const char[] demoFileName) { + DataPack pack = CreateDataPack(); + pack.WriteString(matchId); + pack.WriteCell(mapNumber); + pack.WriteString(demoFileName); + return pack; +} + +static void ReadDemoDataPack(DataPack pack, char[] matchId, const int matchIdLength, int &mapNumber, + char[] demoFileName, const int demoFileNameLength) { + pack.Reset(); + pack.ReadString(matchId, matchIdLength); + mapNumber = pack.ReadCell(); + pack.ReadString(demoFileName, demoFileNameLength); + delete pack; +} + +static Action Timer_StopGoTVRecording(Handle timer, DataPack pack) { + char matchId[MATCH_ID_LENGTH]; + char demoFileName[PLATFORM_MAX_PATH]; + int mapNumber; + ReadDemoDataPack(pack, matchId, sizeof(matchId), mapNumber, demoFileName, sizeof(demoFileName)); + StopRecordingCallback(matchId, mapNumber, demoFileName); + return Plugin_Handled; +} + +static Action Timer_FireStopRecordingEvent(Handle timer, DataPack pack) { + char matchId[MATCH_ID_LENGTH]; + char demoFileName[PLATFORM_MAX_PATH]; + int mapNumber; + ReadDemoDataPack(pack, matchId, sizeof(matchId), mapNumber, demoFileName, sizeof(demoFileName)); + + Get5DemoFinishedEvent event = new Get5DemoFinishedEvent(matchId, mapNumber, demoFileName); + LogDebug("Calling Get5_OnDemoFinished()"); + Call_StartForward(g_OnDemoFinished); + Call_PushCell(event); + Call_Finish(); + EventLogger_LogAndDeleteEvent(event); + return Plugin_Handled; +} + +static bool IsTVEnabled() { + ConVar tvEnabledCvar = FindConVar("tv_enable"); + if (tvEnabledCvar == null) { + LogError("Failed to get tv_enable cvar"); + return false; + } + if (tvEnabledCvar.BoolValue) { + // GOTV can be enabled without the bot actually running; map restart is + // required, so it might be disabled in edge-cases. + LOOP_CLIENTS(i) { + if (IsClientConnected(i) && IsClientSourceTV(i)) { + return true; + } + } + } + return false; +} + +int GetTvDelay() { + if (IsTVEnabled()) { + return GetCvarIntSafe("tv_delay"); + } + return 0; +} + +float GetCurrentMatchRestartDelay() { + ConVar mp_match_restart_delay = FindConVar("mp_match_restart_delay"); + if (mp_match_restart_delay == INVALID_HANDLE) { + return 1.0; // Shouldn't really be possible, but as a safeguard. + } + return mp_match_restart_delay.FloatValue; +} + +void SetCurrentMatchRestartDelay(float delay) { + ConVar mp_match_restart_delay = FindConVar("mp_match_restart_delay"); + if (mp_match_restart_delay != INVALID_HANDLE) { + mp_match_restart_delay.FloatValue = delay; + } +} diff --git a/scripting/get5/stats.sp b/scripting/get5/stats.sp index 057d62e11..c92e51734 100644 --- a/scripting/get5/stats.sp +++ b/scripting/get5/stats.sp @@ -1,6 +1,4 @@ -const float kTimeGivenToTrade = 1.5; - -public void Stats_PluginStart() { +void Stats_PluginStart() { HookEvent("bomb_defused", Stats_BombDefusedEvent); HookEvent("bomb_exploded", Stats_BombExplodedEvent); HookEvent("bomb_planted", Stats_BombPlantedEvent); @@ -18,9 +16,9 @@ public void Stats_PluginStart() { HookEvent("smokegrenade_detonate", Stats_SmokeGrenadeDetonateEvent); } -public Action HandlePlayerDamage(int victim, int &attacker, int &inflictor, float &damage, - int &damagetype) { - if (g_GameState != Get5State_Live) { +static Action HandlePlayerDamage(int victim, int &attacker, int &inflictor, float &damage, + int &damagetype) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { return Plugin_Continue; } LogDebug("HandlePlayerDamage(victim=%d, attacker=%d, inflictor=%d, damage=%f, damageType=%d)", @@ -123,7 +121,7 @@ public Action HandlePlayerDamage(int victim, int &attacker, int &inflictor, floa return Plugin_Continue; } -public Get5Player GetPlayerObject(int client) { +Get5Player GetPlayerObject(int client) { if (client == 0) { return new Get5Player(0, "", view_as(CS_TEAM_NONE), "Console", false); } @@ -132,9 +130,9 @@ public Get5Player GetPlayerObject(int client) { return new Get5Player(0, "", view_as(CS_TEAM_NONE), "GOTV", false); } - // In cases where users disconnect (Get5PlayerDisconnectedEvent) without being on a team, they might error out - // on GetClientTeam(), so we check that they're in-game before we attempt to determine their team. - // Avoids "Client x is not in game" exception. + // In cases where users disconnect (Get5PlayerDisconnectedEvent) without being on a team, they + // might error out on GetClientTeam(), so we check that they're in-game before we attempt to + // determine their team. Avoids "Client x is not in game" exception. Get5Side side = view_as(IsClientInGame(client) ? GetClientTeam(client) : CS_TEAM_NONE); char name[MAX_NAME_LENGTH]; @@ -147,48 +145,46 @@ public Get5Player GetPlayerObject(int client) { GetAuth(client, auth, sizeof(auth)); return new Get5Player(userId, auth, side, name, false); } else { - char botId[8]; + char botId[10]; Format(botId, sizeof(botId), "BOT-%d", userId); return new Get5Player(userId, botId, side, name, true); } } -public void Stats_HookDamageForClient(int client) { +void Stats_HookDamageForClient(int client) { SDKHook(client, SDKHook_OnTakeDamageAlive, HandlePlayerDamage); LogDebug("Hooked client %d to SDKHook_OnTakeDamageAlive", client); } -public void Stats_Reset() { +void Stats_Reset() { if (g_StatsKv != null) { delete g_StatsKv; } g_StatsKv = new KeyValues("Stats"); } -public void Stats_InitSeries() { +void Stats_InitSeries() { Stats_Reset(); char seriesType[32]; - Format(seriesType, sizeof(seriesType), "bo%d", MaxMapsToPlay(g_MapsToWin)); + Format(seriesType, sizeof(seriesType), "bo%d", g_NumberOfMapsInSeries); g_StatsKv.SetString(STAT_SERIESTYPE, seriesType); g_StatsKv.SetString(STAT_SERIES_TEAM1NAME, g_TeamNames[Get5Team_1]); g_StatsKv.SetString(STAT_SERIES_TEAM2NAME, g_TeamNames[Get5Team_2]); DumpToFile(); } -public void Stats_ResetRoundValues() { +void Stats_ResetRoundValues() { + g_FirstKillDone = false; + g_FirstDeathDone = false; g_SetTeamClutching[CS_TEAM_CT] = false; g_SetTeamClutching[CS_TEAM_T] = false; - g_TeamFirstKillDone[CS_TEAM_CT] = false; - g_TeamFirstKillDone[CS_TEAM_T] = false; - g_TeamFirstDeathDone[CS_TEAM_CT] = false; - g_TeamFirstDeathDone[CS_TEAM_T] = false; - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { Stats_ResetClientRoundValues(i); } } -public void Stats_ResetClientRoundValues(int client) { +void Stats_ResetClientRoundValues(int client) { g_RoundKills[client] = 0; g_RoundClutchingEnemyCount[client] = 0; g_PlayerKilledBy[client] = -1; @@ -196,7 +192,7 @@ public void Stats_ResetClientRoundValues(int client) { g_PlayerRoundKillOrAssistOrTradedDeath[client] = false; g_PlayerSurvived[client] = true; - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { g_DamageDone[client][i] = 0; g_DamageDoneHits[client][i] = 0; g_DamageDoneKill[client][i] = false; @@ -205,7 +201,7 @@ public void Stats_ResetClientRoundValues(int client) { } } -public void Stats_ResetGrenadeContainers() { +void Stats_ResetGrenadeContainers() { LogDebug("Clearing out any lingering events in grenade StringMaps..."); // If any molotovs were active on the previous round when it ended (or on halftime/game end), we @@ -251,24 +247,29 @@ public void Stats_ResetGrenadeContainers() { g_LatestMolotovToExtinguishBySmoke = 0; } -public void Stats_RoundStart() { - for (int i = 1; i <= MaxClients; i++) { +void Stats_RoundStart() { + LOOP_CLIENTS(i) { if (IsPlayer(i)) { + // Ensures that each player has zero-filled stats on freeze-time end. + // Since joining the game after freeze-time will render you dead, you cannot obtain stats + // until next round. + Get5Side side = view_as(GetClientTeam(i)); + if (side == Get5Side_None) { + continue; // Don't do anything to players pending team join. + } Get5Team team = GetClientMatchTeam(i); if (team == Get5Team_1 || team == Get5Team_2) { + InitPlayerStats(i, side); + if (side == Get5Side_Spec) { + continue; // exclude coaches from STAT_ROUNDSPLAYED. + } IncrementPlayerStat(i, STAT_ROUNDSPLAYED); - - GoToPlayer(i); - char name[MAX_NAME_LENGTH]; - GetClientName(i, name, sizeof(name)); - g_StatsKv.SetString(STAT_NAME, name); - GoBackFromPlayer(); } } } } -public void Stats_RoundEnd(int csTeamWinner) { +void Stats_RoundEnd(int csTeamWinner) { // Update team scores. GoToMap(); char mapName[PLATFORM_MAX_PATH]; @@ -285,7 +286,7 @@ public void Stats_RoundEnd(int csTeamWinner) { GoBackFromTeam(); // Update player 1vx, x-kill, and KAST values. - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (IsPlayer(i)) { Get5Team team = GetClientMatchTeam(i); if (team == Get5Team_1 || team == Get5Team_2) { @@ -332,108 +333,97 @@ public void Stats_RoundEnd(int csTeamWinner) { } } } - - if (g_DamagePrintCvar.BoolValue) { - for (int i = 1; i <= MaxClients; i++) { - if (IsValidClient(i)) { - PrintDamageInfo(i); - } - } - } } -public void Stats_UpdateMapScore(Get5Team winner) { +void Stats_UpdateMapScore(Get5Team winner) { GoToMap(); - char winnerString[16]; GetTeamString(winner, winnerString, sizeof(winnerString)); - g_StatsKv.SetString(STAT_MAPWINNER, winnerString); - g_StatsKv.SetString(STAT_DEMOFILENAME, g_DemoFileName); - GoBackFromMap(); + DumpToFile(); +} +void Stats_SetDemoName(const char[] demoFileName) { + GoToMap(); + g_StatsKv.SetString(STAT_DEMOFILENAME, demoFileName); + GoBackFromMap(); DumpToFile(); } -public void Stats_Forfeit(Get5Team team) { +void Stats_Forfeit() { g_StatsKv.SetNum(STAT_SERIES_FORFEIT, 1); - if (team == Get5Team_1) { - Stats_SeriesEnd(Get5Team_2); - } else if (team == Get5Team_2) { - Stats_SeriesEnd(Get5Team_1); - } else { - Stats_SeriesEnd(Get5Team_None); - } } -public void Stats_SeriesEnd(Get5Team winner) { +void Stats_SeriesEnd(Get5Team winner) { char winnerString[16]; GetTeamString(winner, winnerString, sizeof(winnerString)); g_StatsKv.SetString(STAT_SERIESWINNER, winnerString); DumpToFile(); } -public void EndMolotovEvent(const char[] molotovKey) { +static void EndMolotovEvent(const char[] molotovKey) { // Since a molotov can be active when the round is ending, we need to grab the information from it // on both RoundStart // **and** on its expire event. Get5MolotovDetonatedEvent molotovObject; if (g_MolotovContainer.GetValue(molotovKey, molotovObject)) { - molotovObject.EndTime = GetRoundTime(); - - LogDebug("Calling Get5_OnMolotovDetonated()"); - - Call_StartForward(g_OnMolotovDetonated); - Call_PushCell(molotovObject); - Call_Finish(); - - EventLogger_LogAndDeleteEvent(molotovObject); - + if (IsDoingRestoreOrMapChange()) { + delete molotovObject; + } else { + molotovObject.EndTime = GetRoundTime(); + LogDebug("Calling Get5_OnMolotovDetonated()"); + Call_StartForward(g_OnMolotovDetonated); + Call_PushCell(molotovObject); + Call_Finish(); + EventLogger_LogAndDeleteEvent(molotovObject); + } g_MolotovContainer.Remove(molotovKey); } } -public void EndHEEvent(const char[] grenadeKey) { +static void EndHEEvent(const char[] grenadeKey) { Get5HEDetonatedEvent heObject; if (g_HEGrenadeContainer.GetValue(grenadeKey, heObject)) { - LogDebug("Calling Get5_OnHEGrenadeDetonated()"); - - Call_StartForward(g_OnHEGrenadeDetonated); - Call_PushCell(heObject); - Call_Finish(); - - EventLogger_LogAndDeleteEvent(heObject); - + if (IsDoingRestoreOrMapChange()) { + delete heObject; + } else { + LogDebug("Calling Get5_OnHEGrenadeDetonated()"); + Call_StartForward(g_OnHEGrenadeDetonated); + Call_PushCell(heObject); + Call_Finish(); + EventLogger_LogAndDeleteEvent(heObject); + } g_HEGrenadeContainer.Remove(grenadeKey); } } -public void EndFlashbangEvent(const char[] flashKey) { +static void EndFlashbangEvent(const char[] flashKey) { Get5FlashbangDetonatedEvent flashEvent; if (g_FlashbangContainer.GetValue(flashKey, flashEvent)) { - LogDebug("Calling Get5_OnFlashbangDetonated()"); - - Call_StartForward(g_OnFlashbangDetonated); - Call_PushCell(flashEvent); - Call_Finish(); - - EventLogger_LogAndDeleteEvent(flashEvent); - + if (IsDoingRestoreOrMapChange()) { + delete flashEvent; + } else { + LogDebug("Calling Get5_OnFlashbangDetonated()"); + Call_StartForward(g_OnFlashbangDetonated); + Call_PushCell(flashEvent); + Call_Finish(); + EventLogger_LogAndDeleteEvent(flashEvent); + } g_FlashbangContainer.Remove(flashKey); } } -public Action Stats_DecoyStartedEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_DecoyStartedEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } int attacker = GetClientOfUserId(event.GetInt("userid")); if (!IsValidClient(attacker)) { - return Plugin_Continue; + return; } Get5DecoyStartedEvent decoyObject = new Get5DecoyStartedEvent( @@ -446,20 +436,18 @@ public Action Stats_DecoyStartedEvent(Event event, const char[] name, bool dontB Call_Finish(); EventLogger_LogAndDeleteEvent(decoyObject); - - return Plugin_Continue; } -public Action Stats_SmokeGrenadeDetonateEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_SmokeGrenadeDetonateEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } int attacker = GetClientOfUserId(event.GetInt("userid")); if (!IsValidClient(attacker)) { g_LatestMolotovToExtinguishBySmoke = 0; // If someone disconnects after throwing grenade. - return Plugin_Continue; + return; } Get5SmokeDetonatedEvent smokeEvent = new Get5SmokeDetonatedEvent( @@ -474,18 +462,16 @@ public Action Stats_SmokeGrenadeDetonateEvent(Event event, const char[] name, bo // Reset this so other smokes don't get extinguish attribution. g_LatestMolotovToExtinguishBySmoke = 0; - - return Plugin_Continue; } -public Action Stats_MolotovStartBurnEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_MolotovStartBurnEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } if (g_LatestUserIdToDetonateMolotov == 0) { // If user disconnected after throwing the molotov, this will be 0. - return Plugin_Continue; + return; } int entityId = event.GetInt("entityid"); @@ -502,13 +488,11 @@ public Action Stats_MolotovStartBurnEvent(Event event, const char[] name, bool d GetPlayerObject(g_LatestUserIdToDetonateMolotov) // Set in molotov detonate event ), true); - - return Plugin_Continue; } -public Action Stats_MolotovExtinguishedEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_MolotovExtinguishedEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } int entityId = event.GetInt("entityid"); @@ -518,13 +502,13 @@ public Action Stats_MolotovExtinguishedEvent(Event event, const char[] name, boo g_LatestMolotovToExtinguishBySmoke = entityId; LogDebug("Molotov Event: %s, %d", name, entityId); - - return Plugin_Continue; } -public Action Stats_MolotovEndedEvent(Event event, const char[] name, bool dontBroadcast) { +static Action Stats_MolotovEndedEvent(Event event, const char[] name, bool dontBroadcast) { + // No backup check; the event is deleted in EndMolotovEvent to prevent leaks, as this function + // works like the the HE/flash timer callbacks which also do not check for backup state. if (g_GameState != Get5State_Live) { - return Plugin_Continue; + return; } int entityId = event.GetInt("entityid"); @@ -535,13 +519,11 @@ public Action Stats_MolotovEndedEvent(Event event, const char[] name, bool dontB IntToString(entityId, molotovKey, sizeof(molotovKey)); EndMolotovEvent(molotovKey); - - return Plugin_Continue; } -public Action Stats_MolotovDetonateEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_MolotovDetonateEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } int attacker = GetClientOfUserId(event.GetInt("userid")); @@ -551,23 +533,21 @@ public Action Stats_MolotovDetonateEvent(Event event, const char[] name, bool do if (!IsValidClient(attacker)) { // Could happen if someone disconnects after throwing a grenade, but before it pops. g_LatestUserIdToDetonateMolotov = 0; - return Plugin_Continue; + return; } g_LatestUserIdToDetonateMolotov = attacker; - - return Plugin_Continue; } -public Action Stats_FlashbangDetonateEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_FlashbangDetonateEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } int attacker = GetClientOfUserId(event.GetInt("userid")); if (!IsValidClient(attacker)) { - return Plugin_Continue; + return; } int entityId = event.GetInt("entityid"); @@ -580,11 +560,9 @@ public Action Stats_FlashbangDetonateEvent(Event event, const char[] name, bool g_FlashbangContainer.SetValue(flashKey, flashEvent, true); CreateTimer(0.001, Timer_HandleFlashbang, entityId, TIMER_FLAG_NO_MAPCHANGE); - - return Plugin_Continue; } -public Action Timer_HandleFlashbang(Handle timer, int entityId) { +static Action Timer_HandleFlashbang(Handle timer, int entityId) { char flashKey[16]; IntToString(entityId, flashKey, sizeof(flashKey)); @@ -593,15 +571,15 @@ public Action Timer_HandleFlashbang(Handle timer, int entityId) { return Plugin_Handled; } -public Action Stats_HEGrenadeDetonateEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_HEGrenadeDetonateEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } int attacker = GetClientOfUserId(event.GetInt("userid")); if (!IsValidClient(attacker)) { - return Plugin_Continue; + return; } int entityId = event.GetInt("entityid"); @@ -614,11 +592,9 @@ public Action Stats_HEGrenadeDetonateEvent(Event event, const char[] name, bool g_HEGrenadeContainer.SetValue(grenadeKey, grenadeObject, true); CreateTimer(0.001, Timer_HandleHEGrenade, entityId, TIMER_FLAG_NO_MAPCHANGE); - - return Plugin_Continue; } -public Action Timer_HandleHEGrenade(Handle timer, int entityId) { +static Action Timer_HandleHEGrenade(Handle timer, int entityId) { char grenadeKey[16]; IntToString(entityId, grenadeKey, sizeof(grenadeKey)); @@ -627,15 +603,15 @@ public Action Timer_HandleHEGrenade(Handle timer, int entityId) { return Plugin_Handled; } -public Action Stats_GrenadeThrownEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_GrenadeThrownEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } int attacker = GetClientOfUserId(event.GetInt("userid")); if (!IsValidClient(attacker)) { - return Plugin_Continue; + return; } char weapon[32]; @@ -652,19 +628,20 @@ public Action Stats_GrenadeThrownEvent(Event event, const char[] name, bool dont Call_Finish(); EventLogger_LogAndDeleteEvent(grenadeEvent); - - return Plugin_Continue; } -public Action Stats_PlayerDeathEvent(Event event, const char[] name, bool dontBroadcast) { +static Action Stats_PlayerDeathEvent(Event event, const char[] name, bool dontBroadcast) { + if (IsDoingRestoreOrMapChange()) { + return; + } int attacker = GetClientOfUserId(event.GetInt("attacker")); - if (g_GameState != Get5State_Live || g_DoingBackupRestoreNow) { + if (g_GameState != Get5State_Live) { if (g_AutoReadyActivePlayersCvar.BoolValue && IsAuthedPlayer(attacker)) { // HandleReadyCommand checks for game state, so we don't need to do that here as well. HandleReadyCommand(attacker, true); } - return Plugin_Continue; + return; } int victim = GetClientOfUserId(event.GetInt("userid")); @@ -675,8 +652,7 @@ public Action Stats_PlayerDeathEvent(Event event, const char[] name, bool dontBr bool validAssister = IsValidClient(assister); if (!validVictim) { - return Plugin_Continue; // Not sure how this would happen, but it's not something we care - // about. + return; // Not sure how this would happen, but it's not something we care about. } // Update "clutch" (1vx) data structures to check if the clutcher wins the round @@ -710,8 +686,9 @@ public Action Stats_PlayerDeathEvent(Event event, const char[] name, bool dontBr // falling from vertigo is attacker 0, weapon id 0, weapon "trigger_hurt" // c4 is attacker 0, weapon id 0, weapon planted_c4 // killing self with weapons is attacker == victim - // some weapons, such as unsilenced USP or M4A1S and molotov fire are also weapon 0, so weapon ID is unreliable. - // with those in mind, we can determine that suicide must be true if attacker is 0 or attacker == victim and it was **not** the bomb. + // some weapons, such as unsilenced USP or M4A1S and molotov fire are also weapon 0, so weapon ID + // is unreliable. with those in mind, we can determine that suicide must be true if attacker is 0 + // or attacker == victim and it was **not** the bomb. bool killedByBomb = StrEqual("planted_c4", weapon); bool isSuicide = (!validAttacker || attacker == victim) && !killedByBomb; @@ -719,8 +696,8 @@ public Action Stats_PlayerDeathEvent(Event event, const char[] name, bool dontBr // used for calculating round KAST g_PlayerSurvived[victim] = false; - if (!g_TeamFirstDeathDone[victimTeam]) { - g_TeamFirstDeathDone[victimTeam] = true; + if (!g_FirstDeathDone) { + g_FirstDeathDone = true; IncrementPlayerStat(victim, (victimTeam == CS_TEAM_CT) ? STAT_FIRSTDEATH_CT : STAT_FIRSTDEATH_T); } @@ -731,8 +708,8 @@ public Action Stats_PlayerDeathEvent(Event event, const char[] name, bool dontBr if (attackerTeam == victimTeam) { IncrementPlayerStat(attacker, STAT_TEAMKILLS); } else { - if (!g_TeamFirstKillDone[attackerTeam]) { - g_TeamFirstKillDone[attackerTeam] = true; + if (!g_FirstKillDone) { + g_FirstKillDone = true; IncrementPlayerStat(attacker, (attackerTeam == CS_TEAM_CT) ? STAT_FIRSTKILL_CT : STAT_FIRSTKILL_T); } @@ -762,10 +739,11 @@ public Action Stats_PlayerDeathEvent(Event event, const char[] name, bool dontBr } Get5PlayerDeathEvent playerDeathEvent = new Get5PlayerDeathEvent( - g_MatchID, g_MapNumber, g_RoundNumber, GetRoundTime(), - GetPlayerObject(victim), new Get5Weapon(weapon, weaponId), headshot, validAttacker ? attackerTeam == victimTeam : false, - event.GetBool("thrusmoke"), event.GetBool("noscope"), event.GetBool("attackerblind"), - isSuicide, event.GetInt("penetrated"), killedByBomb); + g_MatchID, g_MapNumber, g_RoundNumber, GetRoundTime(), GetPlayerObject(victim), + new Get5Weapon(weapon, weaponId), headshot, + validAttacker ? attackerTeam == victimTeam : false, event.GetBool("thrusmoke"), + event.GetBool("noscope"), event.GetBool("attackerblind"), isSuicide, + event.GetInt("penetrated"), killedByBomb); if (validAttacker) { playerDeathEvent.Attacker = GetPlayerObject(attacker); @@ -799,17 +777,15 @@ public Action Stats_PlayerDeathEvent(Event event, const char[] name, bool dontBr Call_Finish(); EventLogger_LogAndDeleteEvent(playerDeathEvent); - - return Plugin_Continue; } static void UpdateTradeStat(int attacker, int victim) { + int attackerTeam = GetClientTeam(attacker); // Look to see if victim killed any of attacker's teammates recently. - for (int i = 1; i <= MaxClients; i++) { - if (IsPlayer(i) && g_PlayerKilledBy[i] == victim && - GetClientTeam(i) == GetClientTeam(attacker)) { + LOOP_CLIENTS(i) { + if (IsPlayer(i) && g_PlayerKilledBy[i] == victim && GetClientTeam(i) == attackerTeam) { float dt = GetGameTime() - g_PlayerKilledByTime[i]; - if (dt < kTimeGivenToTrade) { + if (dt < 1.5) { // "Time to trade" window fixed to 1.5 seconds. IncrementPlayerStat(attacker, STAT_TRADEKILL); // teammate (i) was traded g_PlayerRoundKillOrAssistOrTradedDeath[i] = true; @@ -818,9 +794,9 @@ static void UpdateTradeStat(int attacker, int victim) { } } -public Action Stats_BombPlantedEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_BombPlantedEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } g_BombPlantedTime = GetEngineTime(); @@ -843,13 +819,11 @@ public Action Stats_BombPlantedEvent(Event event, const char[] name, bool dontBr EventLogger_LogAndDeleteEvent(bombEvent); } - - return Plugin_Continue; } -public Action Stats_BombDefusedEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_BombDefusedEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } int client = GetClientOfUserId(event.GetInt("userid")); @@ -876,13 +850,11 @@ public Action Stats_BombDefusedEvent(Event event, const char[] name, bool dontBr EventLogger_LogAndDeleteEvent(defuseEvent); } - - return Plugin_Continue; } -public Action Stats_BombExplodedEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_BombExplodedEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } Get5BombExplodedEvent bombExplodedEvent = new Get5BombExplodedEvent( @@ -895,13 +867,11 @@ public Action Stats_BombExplodedEvent(Event event, const char[] name, bool dontB Call_Finish(); EventLogger_LogAndDeleteEvent(bombExplodedEvent); - - return Plugin_Continue; } -public Action Stats_PlayerBlindEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_PlayerBlindEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } float duration = event.GetFloat("blind_duration"); @@ -909,12 +879,12 @@ public Action Stats_PlayerBlindEvent(Event event, const char[] name, bool dontBr int attacker = GetClientOfUserId(event.GetInt("attacker")); if (!IsValidClient(attacker) || !IsValidClient(victim)) { - return Plugin_Continue; + return; } int victimTeam = GetClientTeam(victim); if (victimTeam == CS_TEAM_SPECTATOR || victimTeam == CS_TEAM_NONE) { - return Plugin_Continue; + return; } bool friendlyFire = GetClientTeam(attacker) == victimTeam; @@ -937,13 +907,11 @@ public Action Stats_PlayerBlindEvent(Event event, const char[] name, bool dontBr new Get5BlindedGrenadeVictim(GetPlayerObject(victim), friendlyFire, duration)); } } - - return Plugin_Continue; } -public Action Stats_RoundMVPEvent(Event event, const char[] name, bool dontBroadcast) { - if (g_GameState != Get5State_Live) { - return Plugin_Continue; +static Action Stats_RoundMVPEvent(Event event, const char[] name, bool dontBroadcast) { + if (g_GameState != Get5State_Live || IsDoingRestoreOrMapChange()) { + return; } int client = GetClientOfUserId(event.GetInt("userid")); @@ -962,8 +930,6 @@ public Action Stats_RoundMVPEvent(Event event, const char[] name, bool dontBroad EventLogger_LogAndDeleteEvent(mvpEvent); } - - return Plugin_Continue; } static int GetPlayerStat(int client, const char[] field) { @@ -980,22 +946,86 @@ static int SetPlayerStat(int client, const char[] field, int newValue) { return newValue; } -public int AddToPlayerStat(int client, const char[] field, int delta) { +static void InitPlayerStats(int client, Get5Side side) { + if (!GoToPlayer(client)) { + return; + } + + // Always update the name. + char name[MAX_NAME_LENGTH]; + GetClientName(client, name, sizeof(name)); + g_StatsKv.SetString(STAT_NAME, name); + + // Update if client is coaching. Spectators are excluded as their match team is spec; this checks + // side only. + g_StatsKv.SetNum(STAT_COACHING, side == Get5Side_Spec); + + // If the player already had their stats set, don't override them. + if (g_StatsKv.GetNum(STAT_INIT, 0) > 0) { + GoBackFromPlayer(); + return; + } + + char keys[][] = {STAT_KILLS, + STAT_DEATHS, + STAT_ASSISTS, + STAT_FLASHBANG_ASSISTS, + STAT_TEAMKILLS, + STAT_SUICIDES, + STAT_DAMAGE, + STAT_UTILITY_DAMAGE, + STAT_ENEMIES_FLASHED, + STAT_FRIENDLIES_FLASHED, + STAT_KNIFE_KILLS, + STAT_HEADSHOT_KILLS, + STAT_ROUNDSPLAYED, + STAT_BOMBDEFUSES, + STAT_BOMBPLANTS, + STAT_1K, + STAT_2K, + STAT_3K, + STAT_4K, + STAT_5K, + STAT_V1, + STAT_V2, + STAT_V3, + STAT_V4, + STAT_V5, + STAT_FIRSTKILL_T, + STAT_FIRSTKILL_CT, + STAT_FIRSTDEATH_T, + STAT_FIRSTDEATH_CT, + STAT_TRADEKILL, + STAT_KAST, + STAT_CONTRIBUTION_SCORE, + STAT_MVP}; + + int length = sizeof(keys); + for (int i = 0; i < length; i++) { + g_StatsKv.SetNum(keys[i], 0); + } + + g_StatsKv.SetNum(STAT_INIT, 1); + + GoBackFromPlayer(); +} + +int AddToPlayerStat(int client, const char[] field, int delta) { if (IsFakeClient(client)) { return 0; } + LogDebug("Updating player stat %s for %L", field, client); int value = GetPlayerStat(client, field); return SetPlayerStat(client, field, value + delta); } static int IncrementPlayerStat(int client, const char[] field) { - LogDebug("Incrementing player stat %s for %L", field, client); return AddToPlayerStat(client, field, 1); } static void GoToMap() { char mapNumberString[32]; - Format(mapNumberString, sizeof(mapNumberString), "map%d", GetMapStatsNumber()); + Format(mapNumberString, sizeof(mapNumberString), "map%d", g_MapNumber); g_StatsKv.JumpToKey(mapNumberString, true); } @@ -1003,13 +1033,17 @@ static void GoBackFromMap() { g_StatsKv.GoBack(); } -static void GoToTeam(Get5Team team) { +static bool GoToTeam(Get5Team team) { GoToMap(); - if (team == Get5Team_1) + if (team == Get5Team_1) { g_StatsKv.JumpToKey("team1", true); - else + return true; + } else if (team == Get5Team_2) { g_StatsKv.JumpToKey("team2", true); + return true; + } + return false; } static void GoBackFromTeam() { @@ -1017,14 +1051,18 @@ static void GoBackFromTeam() { g_StatsKv.GoBack(); } -static void GoToPlayer(int client) { +static bool GoToPlayer(int client) { Get5Team team = GetClientMatchTeam(client); - GoToTeam(team); + if (!GoToTeam(team)) { + return false; + } char auth[AUTH_LENGTH]; if (GetAuth(client, auth, sizeof(auth))) { g_StatsKv.JumpToKey(auth, true); + return true; } + return false; } static void GoBackFromPlayer() { @@ -1032,18 +1070,10 @@ static void GoBackFromPlayer() { g_StatsKv.GoBack(); } -public int GetMapStatsNumber() { - int x = Get5_GetMapNumber(); - if (g_MapChangePending) { - x--; - } - return x; -} - static int GetClutchingClient(int csTeam) { int client = -1; int count = 0; - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (IsPlayer(i) && IsPlayerAlive(i) && GetClientTeam(i) == csTeam) { client = i; count++; @@ -1057,18 +1087,18 @@ static int GetClutchingClient(int csTeam) { } } -public void DumpToFile() { +static void DumpToFile() { char path[PLATFORM_MAX_PATH + 1]; if (FormatCvarString(g_StatsPathFormatCvar, path, sizeof(path))) { DumpToFilePath(path); } } -public bool DumpToFilePath(const char[] path) { +bool DumpToFilePath(const char[] path) { return IsJSONPath(path) ? DumpToJSONFile(path) : g_StatsKv.ExportToFile(path); } -public bool DumpToJSONFile(const char[] path) { +static bool DumpToJSONFile(const char[] path) { g_StatsKv.Rewind(); g_StatsKv.GotoFirstSubKey(false); JSON_Object stats = EncodeKeyValue(g_StatsKv); @@ -1093,7 +1123,7 @@ public bool DumpToJSONFile(const char[] path) { return true; } -JSON_Object EncodeKeyValue(KeyValues kv) { +static JSON_Object EncodeKeyValue(KeyValues kv) { char keyBuffer[256]; char valBuffer[256]; char sectionName[256]; @@ -1126,28 +1156,30 @@ JSON_Object EncodeKeyValue(KeyValues kv) { return json_kv; } -static void PrintDamageInfo(int client) { - if (!IsPlayer(client)) +void PrintDamageInfo(int client) { + if (!IsPlayer(client)) { return; + } int team = GetClientTeam(client); - if (team != CS_TEAM_T && team != CS_TEAM_CT) + if (team != CS_TEAM_T && team != CS_TEAM_CT) { return; + } char message[256]; int msgSize = sizeof(message); int otherTeam = (team == CS_TEAM_T) ? CS_TEAM_CT : CS_TEAM_T; - for (int i = 1; i <= MaxClients; i++) { - if (IsValidClient(i) && IsClientInGame(i) && GetClientTeam(i) == otherTeam) { + + LOOP_CLIENTS(i) { + if (IsValidClient(i) && GetClientTeam(i) == otherTeam) { int health = IsPlayerAlive(i) ? GetClientHealth(i) : 0; char name[64]; GetClientName(i, name, sizeof(name)); g_DamagePrintFormatCvar.GetString(message, msgSize); ReplaceStringWithInt(message, msgSize, "{DMG_TO}", g_DamageDone[client][i], false); - ReplaceStringWithInt(message, msgSize, "{HITS_TO}", g_DamageDoneHits[client][i], - false); + ReplaceStringWithInt(message, msgSize, "{HITS_TO}", g_DamageDoneHits[client][i], false); if (g_DamageDoneKill[client][i]) { ReplaceString(message, msgSize, "{KILL_TO}", "{GREEN}X{NORMAL}", false); @@ -1160,8 +1192,7 @@ static void PrintDamageInfo(int client) { } ReplaceStringWithInt(message, msgSize, "{DMG_FROM}", g_DamageDone[i][client], false); - ReplaceStringWithInt(message, msgSize, "{HITS_FROM}", g_DamageDoneHits[i][client], - false); + ReplaceStringWithInt(message, msgSize, "{HITS_FROM}", g_DamageDoneHits[i][client], false); if (g_DamageDoneKill[i][client]) { ReplaceString(message, msgSize, "{KILL_FROM}", "{DARK_RED}X{NORMAL}", false); diff --git a/scripting/get5/teamlogic.sp b/scripting/get5/teamlogic.sp index a90e1eaca..2bfbf502a 100644 --- a/scripting/get5/teamlogic.sp +++ b/scripting/get5/teamlogic.sp @@ -1,236 +1,235 @@ -public Action Command_JoinGame(int client, const char[] command, int argc) { - if (g_GameState != Get5State_None && g_CheckAuthsCvar.BoolValue && IsPlayer(client) && !g_PendingSideSwap) { - // In order to avoid duplication of team-join logic, we directly call the same handle that would be called - // if the user selected any team after joining. Since Command_JoinTeam handles the actual joining using a - // FakeClientCommand, we don't have to do any team-logic here and it won't matter what we pass to Command_JoinTeam. - // The only thing that's important is that the command argument is empty, as that avoids a call to GetCmdArg in that function. - CreateTimer(0.1, Timer_PlacePlayerOnJoin, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); +Action Command_JoinGame(int client, const char[] command, int argc) { + LogDebug("Client %d sent joingame command.", client); + if (CheckAutoLoadConfig()) { + // Autoload places players on teams. + return; + } + if (g_GameState != Get5State_None && g_CheckAuthsCvar.BoolValue && IsPlayer(client)) { + PlacePlayerOnTeam(client); } - return Plugin_Continue; } -public Action Timer_PlacePlayerOnJoin(Handle timer, int userId) { - int client = GetClientOfUserId(userId); - if (client) { // Client might have disconnected between timer and callback. - Command_JoinTeam(client, "", 1); +void CheckClientTeam(int client, bool useDefaultTeamSelection = true) { + if (!g_CheckAuthsCvar.BoolValue || IsFakeClient(client)) { + // Teams are not enforced; do nothing. + return; } -} -public void CheckClientTeam(int client) { Get5Team correctTeam = GetClientMatchTeam(client); - char auth[AUTH_LENGTH]; - int csTeam = Get5TeamToCSTeam(correctTeam); - int currentTeam = GetClientTeam(client); - - if (csTeam != currentTeam) { - if (IsClientCoaching(client)) { - UpdateCoachTarget(client, csTeam); - } else if (GetAuth(client, auth, sizeof(auth))) { - char steam64[AUTH_LENGTH]; - ConvertAuthToSteam64(auth, steam64); - if (IsAuthOnTeamCoach(steam64, correctTeam)) { - UpdateCoachTarget(client, csTeam); - } - } - - SwitchPlayerTeam(client, csTeam); + if (correctTeam == Get5Team_None) { + RememberAndKickClient(client, "%t", "YouAreNotAPlayerInfoMessage"); + return; } -} - -public Action Command_JoinTeam(int client, const char[] command, int argc) { - if (!IsAuthedPlayer(client) || argc < 1) - return Plugin_Stop; - // Don't do anything if not live/not in startup phase. - if (g_GameState == Get5State_None) { - return Plugin_Continue; + Get5Side correctSide = view_as(Get5TeamToCSTeam(correctTeam)); + if (correctSide == Get5Side_None) { + // This should not be possible. + LogError("Client %d belongs to no side. This is an unexpected error and should be reported.", + client); + return; } - // Don't enforce team joins. - if (!g_CheckAuthsCvar.BoolValue) { - return Plugin_Continue; + int coachesOnTeam = CountCoachesOnTeam(correctTeam, client); + // If the player is fixed to coaching, always ensure they end there and on the correct side. + if (g_CoachingEnabledCvar.BoolValue && IsClientCoachForTeam(client, correctTeam)) { + // If there are free coach spots on the team, send the player there + if (coachesOnTeam < g_CoachesPerTeam) { + SetClientCoaching(client, correctSide); + } else { + KickClient(client, "%t", "TeamIsFullInfoMessage"); + } + return; } - if (g_PendingSideSwap) { - LogDebug("Blocking teamjoin due to pending swap"); - return Plugin_Stop; + // If player was not locked to coaching, check if their team's current size -self is less than the + // max. + if (CountPlayersOnTeam(correctTeam, client) < g_PlayersPerTeam) { + SwitchPlayerTeam(client, correctSide, useDefaultTeamSelection); + return; } - Get5Team correctTeam = GetClientMatchTeam(client); - int csTeam = Get5TeamToCSTeam(correctTeam); - - // This is required as it avoids an exception due to calling this function - // from Timer_PlacePlayerOnJoin, which gets called from Command_JoinGame. - if (!StrEqual("", command)) { - char arg[4]; - int team_to; - GetCmdArg(1, arg, sizeof(arg)); - team_to = StringToInt(arg); - - LogDebug("%L jointeam command, from %d to %d", client, GetClientTeam(client), team_to); - - // don't let someone change to a "none" team (e.g. using auto-select) - if (team_to == CS_TEAM_NONE) { - return Plugin_Stop; - } - - if (csTeam == team_to) { - if (CheckIfClientCoachingAndMoveToCoach(client, correctTeam)) { - return Plugin_Stop; - } else { - return Plugin_Continue; - } + // We end here if a player was not a predefined coach while there was no space as a regular + // player. If coaching is enabled, we drop the player in coach, and if not, they must be kicked. + if (g_CoachingEnabledCvar.BoolValue && coachesOnTeam < g_CoachesPerTeam) { + Get5_Message(client, "%t", "MoveToCoachInfoMessage"); + // In scrim mode, we don't put coaches or players of the "away" team into any auth arrays; they + // default to the opposite of the home team. If a full team's coach disconnects or leaves and + // rejoins, they should be placed on the coach team if their team is full. In a regular match, + // they will have called .coach before the map starts and will be placed by auth above. + if (!g_InScrimMode) { + MovePlayerToCoachInConfig(client, correctTeam); } + SetClientCoaching(client, correctSide); + return; } + KickClient(client, "%t", "TeamIsFullInfoMessage"); +} - LogDebug("jointeam, gamephase = %d", GetGamePhase()); - - if (csTeam != GetClientTeam(client)) { - int count = CountPlayersOnCSTeam(csTeam); - - if (count >= g_PlayersPerTeam) { - if (!g_CoachingEnabledCvar.BoolValue) { - KickClient(client, "%t", "TeamIsFullInfoMessage"); - } else { - // Only attempt to move to coach if we are not full on coaches already. - if (GetTeamCoaches(correctTeam).Length <= g_CoachesPerTeam) { - LogDebug("Forcing player %N to coach", client); - MoveClientToCoach(client); - Get5_Message(client, "%t", "MoveToCoachInfoMessage"); - } else { - KickClient(client, "%t", "TeamIsFullInfoMessage"); - } - } - } else if (!CheckIfClientCoachingAndMoveToCoach(client, correctTeam)) { - LogDebug("Forcing player %N onto %d", client, csTeam); - FakeClientCommand(client, "jointeam %d", csTeam); - } - - return Plugin_Stop; +static void PlacePlayerOnTeam(int client) { + if (g_PendingSideSwap || InHalftimePhase()) { + LogDebug("Blocking attempt to join a team due to halftime or pending team swap."); + return; } - - return Plugin_Stop; + CheckClientTeam(client); } -public bool CheckIfClientCoachingAndMoveToCoach(int client, Get5Team team) { - if (!g_CoachingEnabledCvar.BoolValue) { - return false; +Action Command_JoinTeam(int client, const char[] command, int argc) { + if (g_GameState == Get5State_None || !g_CheckAuthsCvar.BoolValue) { + return Plugin_Continue; } - // Force user to join the coach if specified by config or reconnect. - char clientAuth64[AUTH_LENGTH]; - GetAuth(client, clientAuth64, AUTH_LENGTH); - if (IsAuthOnTeamCoach(clientAuth64, team)) { - LogDebug("Forcing player %N to coach as they were previously.", client); - MoveClientToCoach(client); - return true; + // If, in some odd case, a player should find themselves on no team while g_CheckAuthsCvar is + // true, we want to let them trigger the PlacePlayerOnTeam logic when clicking any team. In any + // other case, we just block. Blocking ensures that coaches in scrim-mode will not stop coaching + // if they select a team in the menu. + if (IsAuthedPlayer(client) && GetClientTeam(client) == CS_TEAM_NONE) { + PlacePlayerOnTeam(client); } - return false; + return Plugin_Stop; } -public void MoveClientToCoach(int client) { - LogDebug("MoveClientToCoach %L", client); - Get5Team matchTeam = GetClientMatchTeam(client); - if (matchTeam != Get5Team_1 && matchTeam != Get5Team_2) { - return; - } +static bool IsClientCoachForTeam(int client, Get5Team team) { + char clientAuth64[AUTH_LENGTH]; + return GetAuth(client, clientAuth64, AUTH_LENGTH) && IsAuthOnTeamCoach(clientAuth64, team); +} - if (!g_CoachingEnabledCvar.BoolValue) { +void SetClientCoaching(int client, Get5Side side) { + if (GetClientCoachingSide(client) == side) { return; } + LogDebug("Setting client %d as spectator and coach for side %d.", client, side); + SwitchPlayerTeam(client, Get5Side_Spec); + SetEntProp(client, Prop_Send, "m_iCoachingTeam", side); + SetEntProp(client, Prop_Send, "m_iObserverMode", 4); + SetEntProp(client, Prop_Send, "m_iAccount", + 0); // Ensures coaches have no money if they were to rejoin the game. + + char formattedPlayerName[MAX_NAME_LENGTH]; + Get5Team team = GetClientMatchTeam(client); + FormatPlayerName(formattedPlayerName, sizeof(formattedPlayerName), client, team); + Get5_MessageToAll("%t", "PlayerIsCoachingTeam", formattedPlayerName, g_FormattedTeamNames[team]); +} - int csTeam = Get5TeamToCSTeam(matchTeam); - - if (g_PendingSideSwap) { - LogDebug("Blocking coach move due to pending swap"); +void CoachingChangedHook(ConVar convar, const char[] oldValue, const char[] newValue) { + if (g_GameState == Get5State_None) { return; } - - char teamString[4]; - char clientAuth[64]; - CSTeamString(csTeam, teamString, sizeof(teamString)); - GetAuth(client, clientAuth, AUTH_LENGTH); - if (!IsAuthOnTeamCoach(clientAuth, matchTeam)) { - AddCoachToTeam(clientAuth, matchTeam, ""); - // If we're already on the team, make sure we remove ourselves - // to ensure data is correct in the backups. - int index = GetTeamAuths(matchTeam).FindString(clientAuth); - if (index >= 0) { - GetTeamAuths(matchTeam).Erase(index); + // If disabling coaching, make sure we swap coaches to team or kick them, as they are now regular + // spectators. + if (StringToInt(oldValue) != 0 && !convar.BoolValue) { + LogDebug("Detected sv_coaching_enabled was disabled. Checking for coaches."); + LOOP_CLIENTS(i) { + if (IsPlayer(i) && IsClientCoaching(i)) { + CheckClientTeam(i); + } } } - - // If we're in warmup we use the in-game - // coaching command. Otherwise we manually move them to spec - // and set the coaching target. - // If in freeze time, we have to manually move as well. - if (!InWarmup() && InFreezeTime()) { - LogDebug("Moving %L directly to coach slot", client); - SwitchPlayerTeam(client, CS_TEAM_SPECTATOR); - UpdateCoachTarget(client, csTeam); - // Need to set to avoid third person view bug. - SetEntProp(client, Prop_Send, "m_iObserverMode", 4); - } else { - LogDebug("Moving %L indirectly to coach slot via coach cmd", client); - g_MovingClientToCoach[client] = true; - FakeClientCommand(client, "coach %s", teamString); - g_MovingClientToCoach[client] = false; - } } -public Action Command_SmCoach(int client, int args) { - char auth[AUTH_LENGTH]; +Action Command_SmCoach(int client, int args) { if (g_GameState == Get5State_None) { return Plugin_Continue; } if (!g_CoachingEnabledCvar.BoolValue) { - return Plugin_Handled; + char formattedCoachingCvar[64]; + FormatCvarName(formattedCoachingCvar, sizeof(formattedCoachingCvar), "sv_coaching_enabled"); + Get5_Message(client, "%t", "CoachingNotEnabled", formattedCoachingCvar); + return Plugin_Continue; } - GetAuth(client, auth, sizeof(auth)); Get5Team matchTeam = GetClientMatchTeam(client); - // Don't allow a new coach if spots are full. - if (GetTeamCoaches(matchTeam).Length > g_CoachesPerTeam) { - return Plugin_Stop; - } - MoveClientToCoach(client); - // Update the backup structure as well for round restores, covers edge - // case of users joining, coaching, stopping, and getting 16k cash as player. - WriteBackup(); - return Plugin_Handled; -} + if (matchTeam == Get5Team_None) { + return Plugin_Continue; + } -public Action Command_Coach(int client, const char[] command, int argc) { - if (g_GameState == Get5State_None) { + if (g_GameState > Get5State_Warmup) { + Get5_Message(client, "%t", "CanOnlyCoachDuringWarmup"); return Plugin_Continue; } - if (!g_CoachingEnabledCvar.BoolValue) { - return Plugin_Handled; + // These counts are excluding the client, so >=. + bool coachSlotsFull = CountCoachesOnTeam(matchTeam, client) >= g_CoachesPerTeam; + bool playerSlotsFull = CountPlayersOnTeam(matchTeam, client) >= g_PlayersPerTeam; + + // If we're in scrim mode, we don't update the coaches auth array ever. + if (g_InScrimMode) { + if (IsClientCoaching(client)) { + if (playerSlotsFull) { + Get5_Message(client, "%t", "CannotLeaveCoachingTeamIsFull"); + return Plugin_Continue; + } + // Fall-through to CheckClientTeam(i) below, which moves the player back on the team because + // they are not defined as a coach in auth. + } else { + if (coachSlotsFull) { + Get5_Message(client, "%t", "AllCoachSlotsFilledForTeam", g_CoachesPerTeam); + return Plugin_Continue; + } + // We use SetClientCoaching instead of fall-though because of missing auth. + SetClientCoaching(client, view_as(Get5TeamToCSTeam(matchTeam))); + return Plugin_Continue; + } + } else { + if (IsClientCoachForTeam(client, matchTeam)) { + if (playerSlotsFull) { + Get5_Message(client, "%t", "CannotLeaveCoachingTeamIsFull"); + return Plugin_Continue; + } + MoveCoachToPlayerInConfig(client, matchTeam); + } else { + if (coachSlotsFull) { + Get5_Message(client, "%t", "AllCoachSlotsFilledForTeam", g_CoachesPerTeam); + return Plugin_Continue; + } + MovePlayerToCoachInConfig(client, matchTeam); + } } + // Move the player. This would potentially kick them if we did not perform above checks. + CheckClientTeam(client); + return Plugin_Continue; +} - if (!IsAuthedPlayer(client)) { - return Plugin_Stop; +static void MovePlayerToCoachInConfig(const int client, const Get5Team team) { + char auth[AUTH_LENGTH]; + GetAuth(client, auth, sizeof(auth)); + if (AddCoachToTeam(auth, team, "")) { + // If we're already on the team, make sure we remove ourselves + // to ensure data is correct in the backups. + int index = GetTeamAuths(team).FindString(auth); + if (index >= 0) { + LogDebug("Removing client %d from player team auth array for team %d.", client, team); + GetTeamAuths(team).Erase(index); + } } +} - if (InHalftimePhase()) { - return Plugin_Stop; +static void MoveCoachToPlayerInConfig(const int client, const Get5Team team) { + char auth[AUTH_LENGTH]; + GetAuth(client, auth, sizeof(auth)); + AddPlayerToTeam(auth, team, ""); + // This differs from MovePlayerToCoachInConfig because being in coach array + player array will + // make coaching take precedence, so if you're being added from coach to player, and you're + // already defined as a player, the above function will return false, so we always remove from the + // coach array when moving from coach to player. + int index = GetTeamCoaches(team).FindString(auth); + if (index >= 0) { + LogDebug("Removing client %d from coach team auth array for team %d", client, team); + GetTeamCoaches(team).Erase(index); } +} - if (g_MovingClientToCoach[client] || !g_CheckAuthsCvar.BoolValue) { - LogDebug("Command_Coach: %L, letting pass-through", client); +Action Command_Coach(int client, const char[] command, int argc) { + if (g_GameState == Get5State_None) { return Plugin_Continue; } - - MoveClientToCoach(client); - // Update the backup structure as well for round restores, covers edge - // case of users joining, coaching, stopping, and getting 16k cash as player. - WriteBackup(); + ReplyToCommand( + client, + "Please use .coach in chat or sm_coach instead of the built-in console coach command."); return Plugin_Stop; } -public Get5Team GetClientMatchTeam(int client) { +Get5Team GetClientMatchTeam(int client) { if (!g_CheckAuthsCvar.BoolValue) { return CSTeamToGet5Team(GetClientTeam(client)); } else { @@ -238,7 +237,7 @@ public Get5Team GetClientMatchTeam(int client) { if (GetAuth(client, auth, sizeof(auth))) { Get5Team playerTeam = GetAuthMatchTeam(auth); if (playerTeam == Get5Team_None) { - playerTeam = GetAuthMatchTeamCoach(auth); + playerTeam = GetAuthMatchCoachTeam(auth); } return playerTeam; } else { @@ -247,7 +246,7 @@ public Get5Team GetClientMatchTeam(int client) { } } -public int Get5TeamToCSTeam(Get5Team t) { +int Get5TeamToCSTeam(Get5Team t) { if (t == Get5Team_1) { return g_TeamSide[Get5Team_1]; } else if (t == Get5Team_2) { @@ -259,7 +258,7 @@ public int Get5TeamToCSTeam(Get5Team t) { } } -public Get5Team CSTeamToGet5Team(int csTeam) { +Get5Team CSTeamToGet5Team(int csTeam) { if (csTeam == g_TeamSide[Get5Team_1]) { return Get5Team_1; } else if (csTeam == g_TeamSide[Get5Team_2]) { @@ -271,11 +270,7 @@ public Get5Team CSTeamToGet5Team(int csTeam) { } } -public Get5Team GetAuthMatchTeam(const char[] steam64) { - if (g_GameState == Get5State_None) { - return Get5Team_None; - } - +Get5Team GetAuthMatchTeam(const char[] steam64) { if (g_InScrimMode) { return IsAuthOnTeam(steam64, Get5Team_1) ? Get5Team_1 : Get5Team_2; } @@ -289,7 +284,7 @@ public Get5Team GetAuthMatchTeam(const char[] steam64) { return Get5Team_None; } -public Get5Team GetAuthMatchTeamCoach(const char[] steam64) { +Get5Team GetAuthMatchCoachTeam(const char[] steam64) { if (g_GameState == Get5State_None) { return Get5Team_None; } @@ -307,41 +302,51 @@ public Get5Team GetAuthMatchTeamCoach(const char[] steam64) { return Get5Team_None; } -stock int CountPlayersOnCSTeam(int team, int exclude = -1) { +int CountCoachesOnTeam(Get5Team team, int exclude = -1) { int count = 0; - for (int i = 1; i <= MaxClients; i++) { - if (i != exclude && IsAuthedPlayer(i) && GetClientTeam(i) == team) { + Get5Side side = view_as(Get5TeamToCSTeam(team)); + LOOP_CLIENTS(i) { + if (i != exclude && IsAuthedPlayer(i) && GetClientMatchTeam(i) == team && + GetClientCoachingSide(i) == side) { count++; } } return count; } -stock int CountPlayersOnMatchTeam(Get5Team team, int exclude = -1) { +int CountPlayersOnTeam(Get5Team team, int exclude = -1) { int count = 0; - for (int i = 1; i <= MaxClients; i++) { - if (i != exclude && IsAuthedPlayer(i) && GetClientMatchTeam(i) == team) { + Get5Side side = view_as(Get5TeamToCSTeam(team)); + LOOP_CLIENTS(i) { + if (i != exclude && IsAuthedPlayer(i) && GetClientMatchTeam(i) == team && + view_as(GetClientTeam(i)) == side) { count++; } } return count; } -// Returns the match team a client is the captain of, or MatchTeam_None. -public Get5Team GetCaptainTeam(int client) { - if (client == GetTeamCaptain(Get5Team_1)) { - return Get5Team_1; - } else if (client == GetTeamCaptain(Get5Team_2)) { - return Get5Team_2; - } else { - return Get5Team_None; +bool IsClientCoaching(int client) { + return GetClientCoachingSide(client) != Get5Side_None; +} + +static Get5Side GetClientCoachingSide(int client) { + if (GetClientTeam(client) != CS_TEAM_SPECTATOR) { + return Get5Side_None; + } + int side = GetEntProp(client, Prop_Send, "m_iCoachingTeam"); + if (side == CS_TEAM_CT) { + return Get5Side_CT; + } else if (side == CS_TEAM_T) { + return Get5Side_T; } + return Get5Side_None; } -public int GetTeamCaptain(Get5Team team) { +int GetTeamCaptain(Get5Team team) { // If not forcing auths, take the 1st client on the team. if (!g_CheckAuthsCvar.BoolValue) { - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (IsAuthedPlayer(i) && GetClientMatchTeam(i) == team) { return i; } @@ -362,7 +367,7 @@ public int GetTeamCaptain(Get5Team team) { return -1; } -public int GetNextTeamCaptain(int client) { +int GetNextTeamCaptain(int client) { if (client == g_VetoCaptains[Get5Team_1]) { return g_VetoCaptains[Get5Team_2]; } else { @@ -370,23 +375,23 @@ public int GetNextTeamCaptain(int client) { } } -public ArrayList GetTeamAuths(Get5Team team) { +ArrayList GetTeamAuths(Get5Team team) { return g_TeamAuths[team]; } -public ArrayList GetTeamCoaches(Get5Team team) { +ArrayList GetTeamCoaches(Get5Team team) { return g_TeamCoaches[team]; } -public bool IsAuthOnTeam(const char[] auth, Get5Team team) { +static bool IsAuthOnTeam(const char[] auth, Get5Team team) { return GetTeamAuths(team).FindString(auth) >= 0; } -public bool IsAuthOnTeamCoach(const char[] auth, Get5Team team) { +static bool IsAuthOnTeamCoach(const char[] auth, Get5Team team) { return GetTeamCoaches(team).FindString(auth) >= 0; } -public void SetStartingTeams() { +void SetStartingTeams() { int mapNumber = Get5_GetMapNumber(); if (mapNumber >= g_MapSides.Length || g_MapSides.Get(mapNumber) == SideChoice_KnifeRound) { g_TeamSide[Get5Team_1] = TEAM1_STARTING_SIDE; @@ -405,24 +410,10 @@ public void SetStartingTeams() { g_TeamStartingSide[Get5Team_2] = g_TeamSide[Get5Team_2]; } -public void AddMapScore() { - int currentMapNumber = Get5_GetMapNumber(); - - g_TeamScoresPerMap.Set(currentMapNumber, CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_1)), - view_as(Get5Team_1)); - - g_TeamScoresPerMap.Set(currentMapNumber, CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_2)), - view_as(Get5Team_2)); -} - -public int GetMapScore(int mapNumber, Get5Team team) { +int GetMapScore(int mapNumber, Get5Team team) { return g_TeamScoresPerMap.Get(mapNumber, view_as(team)); } -public bool HasMapScore(int mapNumber) { - return GetMapScore(mapNumber, Get5Team_1) != 0 || GetMapScore(mapNumber, Get5Team_2) != 0; -} - bool AddPlayerToTeam(const char[] auth, Get5Team team, const char[] name) { char steam64[AUTH_LENGTH]; if (!ConvertAuthToSteam64(auth, steam64)) { @@ -449,7 +440,7 @@ bool AddCoachToTeam(const char[] auth, Get5Team team, const char[] name) { return false; } - if (GetAuthMatchTeamCoach(steam64) == Get5Team_None) { + if (GetAuthMatchCoachTeam(steam64) == Get5Team_None) { GetTeamCoaches(team).PushString(steam64); Get5_SetPlayerName(auth, name); return true; @@ -486,7 +477,7 @@ bool RemovePlayerFromTeams(const char[] auth) { return false; } -public void LoadPlayerNames() { +void LoadPlayerNames() { KeyValues namesKv = new KeyValues("Names"); int numNames = 0; LOOP_TEAMS(team) { @@ -496,8 +487,7 @@ public void LoadPlayerNames() { ArrayList coachIds = GetTeamCoaches(team); for (int i = 0; i < ids.Length; i++) { ids.GetString(i, id, sizeof(id)); - if (g_PlayerNames.GetString(id, name, sizeof(name)) && !StrEqual(name, "") && - !StrEqual(name, KEYVALUE_STRING_PLACEHOLDER)) { + if (g_PlayerNames.GetString(id, name, sizeof(name)) && !StrEqual(name, "")) { namesKv.SetString(id, name); numNames++; } @@ -506,8 +496,7 @@ public void LoadPlayerNames() { // There's a way to push an array of cells into the end, however, it // becomes a single element, rather than pushing individually. coachIds.GetString(i, id, sizeof(id)); - if (g_PlayerNames.GetString(id, name, sizeof(name)) && !StrEqual(name, "") && - !StrEqual(name, KEYVALUE_STRING_PLACEHOLDER)) { + if (g_PlayerNames.GetString(id, name, sizeof(name)) && !StrEqual(name, "")) { namesKv.SetString(id, name); numNames++; } @@ -515,19 +504,21 @@ public void LoadPlayerNames() { } if (numNames > 0) { - char nameFile[] = "get5_names.txt"; + char nameFile[PLATFORM_MAX_PATH]; + GetTempFilePath(nameFile, sizeof(nameFile), TEMP_VALVE_NAMES_FILE_PATTERN); DeleteFile(nameFile); if (namesKv.ExportToFile(nameFile)) { ServerCommand("sv_load_forced_client_names_file %s", nameFile); + LogDebug("Wrote %d fixed player name(s) to %s.", numNames, nameFile); } else { - LogError("Failed to write names keyvalue file to %s", nameFile); + LogError("Failed to write fixed player names to %s.", nameFile); } } delete namesKv; } -public void SwapScrimTeamStatus(int client) { +void SwapScrimTeamStatus(int client) { // If we're in any team -> remove from any team list. // If we're not in any team -> add to team1. char auth[AUTH_LENGTH]; @@ -538,6 +529,6 @@ public void SwapScrimTeamStatus(int client) { ConvertAuthToSteam64(auth, steam64); GetTeamAuths(Get5Team_1).PushString(steam64); } + CheckClientTeam(client); } - CheckClientTeam(client); } diff --git a/scripting/get5/tests.sp b/scripting/get5/tests.sp index 418224bd0..778c53faf 100644 --- a/scripting/get5/tests.sp +++ b/scripting/get5/tests.sp @@ -1,9 +1,9 @@ -public Action Command_Test(int args) { +Action Command_Test(int args) { Get5_Test(); return Plugin_Handled; } -public void Get5_Test() { +static void Get5_Test() { if (g_GameState != Get5State_None) { g_GameState = Get5State_None; } @@ -15,16 +15,21 @@ public void Get5_Test() { KV_Test(); g_GameState = Get5State_None; + LogMessage("Tests complete!"); } static void Utils_Test() { SetTestContext("Utils_Test"); - // MaxMapsToPlay - AssertEq("MaxMapsToPlay1", MaxMapsToPlay(1), 1); - AssertEq("MaxMapsToPlay2", MaxMapsToPlay(2), 3); - AssertEq("MaxMapsToPlay3", MaxMapsToPlay(3), 5); - AssertEq("MaxMapsToPlay4", MaxMapsToPlay(4), 7); + // MapsToWin + AssertEq("MapsToWin1", MapsToWin(1), 1); + AssertEq("MapsToWin2", MapsToWin(2), 2); + AssertEq("MapsToWin3", MapsToWin(3), 2); + AssertEq("MapsToWin4", MapsToWin(4), 3); + AssertEq("MapsToWin5", MapsToWin(5), 3); + AssertEq("MapsToWin6", MapsToWin(6), 4); + AssertEq("MapsToWin7", MapsToWin(7), 4); + AssertEq("MapsToWin8", MapsToWin(8), 5); // ConvertAuthToSteam64 char input[64] = "STEAM_0:1:52245092"; @@ -70,9 +75,9 @@ static void KV_Test() { SetTestContext("KV_Test"); AssertEq("maps_to_win", g_MapsToWin, 2); - AssertEq("bo2_series", g_BO2Match, false); + AssertEq("num_maps", g_NumberOfMapsInSeries, 3); AssertEq("skip_veto", g_SkipVeto, false); - AssertEq("players_per_team", g_PlayersPerTeam, 1); + AssertEq("players_per_team", g_PlayersPerTeam, 5); AssertEq("favored_percentage_team1", g_FavoredTeamPercentage, 65); AssertTrue("team1.name", StrEqual(g_TeamNames[Get5Team_1], "EnvyUs", false)); diff --git a/scripting/get5/util.sp b/scripting/get5/util.sp index 9ab377b33..922ba4a37 100644 --- a/scripting/get5/util.sp +++ b/scripting/get5/util.sp @@ -4,19 +4,20 @@ #define MAX_FLOAT_STRING_LENGTH 32 #define AUTH_LENGTH 64 -// Dummy value for when we need to write a keyvalue string, but we don't care about the value. -// Trying to write an empty string often results in the keyvalue not being written, so we use this. +// Dummy value for when we need to write a KeyValue string, but we don't care about the value *or* +// when the value is an empty string. Trying to write an empty string results in the KeyValue not +// being written, so we use this. #define KEYVALUE_STRING_PLACEHOLDER "__placeholder" -static char _colorNames[][] = {"{NORMAL}", "{DARK_RED}", "{PINK}", "{GREEN}", - "{YELLOW}", "{LIGHT_GREEN}", "{LIGHT_RED}", "{GRAY}", - "{ORANGE}", "{LIGHT_BLUE}", "{DARK_BLUE}", "{PURPLE}"}; -static char _colorCodes[][] = {"\x01", "\x02", "\x03", "\x04", "\x05", "\x06", - "\x07", "\x08", "\x09", "\x0B", "\x0C", "\x0E"}; +static char _colorNames[][] = {"{NORMAL}", "{DARK_RED}", "{PINK}", "{GREEN}", "{YELLOW}", + "{LIGHT_GREEN}", "{LIGHT_RED}", "{GRAY}", "{ORANGE}", "{LIGHT_BLUE}", + "{DARK_BLUE}", "{PURPLE}", "{GOLD}"}; +static char _colorCodes[][] = {"\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", + "\x08", "\x09", "\x0B", "\x0C", "\x0E", "\x10"}; // Convenience macros. #define LOOP_TEAMS(%1) for (Get5Team %1 = Get5Team_1; %1 < Get5Team_Count; %1 ++) -#define LOOP_CLIENTS(%1) for (int %1 = 0; %1 <= MaxClients; %1 ++) +#define LOOP_CLIENTS(%1) for (int %1 = 1; %1 <= MaxClients; %1 ++) // These match CS:GO's m_gamePhase values. enum GamePhase { @@ -31,7 +32,7 @@ enum GamePhase { */ stock int GetNumHumansOnTeam(int team) { int count = 0; - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (IsPlayer(i) && GetClientTeam(i) == team) { count++; } @@ -41,7 +42,7 @@ stock int GetNumHumansOnTeam(int team) { stock int CountAlivePlayersOnTeam(int csTeam) { int count = 0; - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (IsPlayer(i) && IsPlayerAlive(i) && GetClientTeam(i) == csTeam) { count++; } @@ -51,7 +52,7 @@ stock int CountAlivePlayersOnTeam(int csTeam) { stock int SumHealthOfTeam(int team) { int sum = 0; - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (IsPlayer(i) && IsPlayerAlive(i) && GetClientTeam(i) == team) { sum += GetClientHealth(i); } @@ -59,21 +60,27 @@ stock int SumHealthOfTeam(int team) { return sum; } -/** - * Switches and respawns a player onto a new team. - */ -stock void SwitchPlayerTeam(int client, int team) { +stock int ConvertCSTeamToDefaultWinReason(int side) { + // This maps to + // https://github.com/VSES/SourceEngine2007/blob/master/se2007/game/shared/cstrike/cs_gamerules.h, + // which is the regular CSRoundEndReason + 1. + return view_as(side == CS_TEAM_CT ? CSRoundEnd_CTWin : CSRoundEnd_TerroristWin) + 1; +} + +stock void SwitchPlayerTeam(int client, Get5Side side, bool useDefaultTeamSelection = true) { + // Check avoids killing player if they're already on the right team. + int team = view_as(side); if (GetClientTeam(client) == team) { return; } - - LogDebug("SwitchPlayerTeam %L to %d", client, team); - if (team > CS_TEAM_SPECTATOR) { + if (useDefaultTeamSelection || team == CS_TEAM_SPECTATOR) { + ChangeClientTeam(client, team); + } else { + // When doing side-swap in knife-rounds, we do this to prevent the score from going -1 for + // everyone. CS_SwitchTeam(client, team); CS_UpdateClientModel(client); CS_RespawnPlayer(client); - } else { - ChangeClientTeam(client, team); } } @@ -81,7 +88,7 @@ stock void SwitchPlayerTeam(int client, int team) { * Returns if a client is valid. */ stock bool IsValidClient(int client) { - return client > 0 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client); + return client > 0 && client <= MaxClients && IsClientInGame(client); } stock bool IsPlayer(int client) { @@ -92,86 +99,12 @@ stock bool IsAuthedPlayer(int client) { return IsPlayer(client) && IsClientAuthorized(client); } -/** - * Used to consistently set string keys on JSON objects that receive a Get5Side parameter, which - * should be output as ct, t, spec or null in JSON. - */ -stock void ConvertGet5SideToStringInJson(const JSON_Object obj, const char[] key, Get5Side side) { - if (side == Get5Side_T) { - obj.SetString(key, "t"); - } else if (side == Get5Side_CT) { - obj.SetString(key, "ct"); - } else if (side == Get5Side_Spec) { - obj.SetString(key, "spec"); - } else { - obj.SetObject(key, null); - } -} - -/** - * Used to consistently set string keys on JSON objects that receive a Get5Team parameter, which - * should be output as team1, team2, spec or null in JSON. - */ -stock void ConvertGet5TeamToStringInJson(const JSON_Object obj, const char[] key, Get5Team team) { - if (team == Get5Team_1) { - obj.SetString(key, "team1"); - } else if (team == Get5Team_2) { - obj.SetString(key, "team2"); - } else if (team == Get5Team_Spec) { - obj.SetString(key, "spec"); - } else { - obj.SetObject(key, null); - } -} - -/** - * Used to consistently map a Get5PauseType to string in JSON. - */ -stock void ConvertGet5PauseTypeToStringInJson(const JSON_Object obj, const char[] key, - Get5PauseType pauseType) { - if (pauseType == Get5PauseType_Admin) { - obj.SetString(key, "admin"); - } else if (pauseType == Get5PauseType_Tech) { - obj.SetString(key, "technical"); - } else if (pauseType == Get5PauseType_Tactical) { - obj.SetString(key, "tactical"); - } else if (pauseType == Get5PauseType_Backup) { - obj.SetString(key, "backup"); - } else { - obj.SetObject(key, null); - } -} - -/** - * Used to consistently convert Get5BombSite to 'a', 'b' or null. - */ -stock void ConvertBombSiteToStringInJson(const JSON_Object obj, const char[] key, - const Get5BombSite site) { - if (site == Get5BombSite_A) { - obj.SetString(key, "a"); - } else if (site == Get5BombSite_B) { - obj.SetString(key, "b"); - } else { - obj.SetObject(key, null); - } -} - -/** - * Used to consistently set string keys on JSON objects that receive a Get5State parameter. - */ -stock void ConvertGameStateToStringInJson(const JSON_Object obj, const char[] key, - const Get5State state) { - char gameStateString[64]; - GameStateString(state, gameStateString, sizeof(gameStateString)); - obj.SetString(key, gameStateString); -} - /** * Returns the number of clients that are actual players in the game. */ stock int GetRealClientCount() { int clients = 0; - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (IsPlayer(i)) { clients++; } @@ -182,68 +115,49 @@ stock int GetRealClientCount() { stock void Colorize(char[] msg, int size, bool stripColor = false) { for (int i = 0; i < sizeof(_colorNames); i++) { if (stripColor) { - ReplaceString(msg, size, _colorNames[i], "\x01"); // replace with white + ReplaceString(msg, size, _colorNames[i], ""); // replace with no color tag } else { ReplaceString(msg, size, _colorNames[i], _colorCodes[i]); } } } -stock void ReplaceStringWithInt(char[] buffer, int len, const char[] replace, int value, - bool caseSensitive = false) { - char intString[MAX_INTEGER_STRING_LENGTH]; - IntToString(value, intString, sizeof(intString)); - ReplaceString(buffer, len, replace, intString, caseSensitive); +stock void FormatChatCommand(char[] buffer, const int bufferLength, const char[] command) { + Format(buffer, bufferLength, "{GREEN}%s{NORMAL}", command); } -stock bool IsTVEnabled() { - ConVar tvEnabledCvar = FindConVar("tv_enable"); - if (tvEnabledCvar == null) { - LogError("Failed to get tv_enable cvar"); - return false; - } - return tvEnabledCvar.BoolValue; +stock void FormatCvarName(char[] buffer, const int bufferLength, const char[] cVar) { + Format(buffer, bufferLength, "{GRAY}%s{NORMAL}", cVar); } -stock int GetTvDelay() { - if (IsTVEnabled()) { - return GetCvarIntSafe("tv_delay"); +stock void FormatPlayerName(char[] buffer, const int bufferLength, const int client, + const Get5Team team) { + // Used when injecting the team for coaching players, who are always on team spectator. + Get5Side side = view_as(Get5_Get5TeamToCSTeam(team)); + if (side == Get5Side_CT) { + Format(buffer, bufferLength, "{LIGHT_BLUE}%N{NORMAL}", client); + } else if (side == Get5Side_T) { + Format(buffer, bufferLength, "{GOLD}%N{NORMAL}", client); + } else { + Format(buffer, bufferLength, "{PURPLE}%N{NORMAL}", client); } - return 0; } -stock bool Record(const char[] demoName) { - char szDemoName[256]; - strcopy(szDemoName, sizeof(szDemoName), demoName); - ReplaceString(szDemoName, sizeof(szDemoName), "\"", "\\\""); - ServerCommand("tv_record \"%s\"", szDemoName); - - if (!IsTVEnabled()) { - LogError( - "Autorecording will not work with current cvar \"tv_enable\"=0. Set \"tv_enable 1\" in server.cfg (or another config file) to fix this."); - return false; - } - - return true; +stock void ReplaceStringWithInt(char[] buffer, int len, const char[] replace, int value, + bool caseSensitive = false) { + char intString[MAX_INTEGER_STRING_LENGTH]; + IntToString(value, intString, sizeof(intString)); + ReplaceString(buffer, len, replace, intString, caseSensitive); } -stock void StopRecording() { - ServerCommand("tv_stoprecord"); - - if (StrEqual("", g_DemoFileName, true)) { - // Demo not recorded; don't fire demo finish event. - return; +stock void AnnouncePhaseChange(const char[] format, const char[] message) { + int count = g_PhaseAnnouncementCountCvar.IntValue; + if (count > 10) { + count = 10; + } + for (int i = 0; i < count; i++) { + Get5_MessageToAll(format, message); } - - Get5DemoFinishedEvent event = new Get5DemoFinishedEvent(g_MatchID, g_MapNumber, g_DemoFileName); - - LogDebug("Calling Get5_OnDemoFinished()"); - - Call_StartForward(g_OnDemoFinished); - Call_PushCell(event); - Call_Finish(); - - EventLogger_LogAndDeleteEvent(event); } stock bool InWarmup() { @@ -258,25 +172,22 @@ stock bool InFreezeTime() { return GameRules_GetProp("m_bFreezePeriod") != 0; } -stock void EnsureIndefiniteWarmup() { - if (!InWarmup()) { - StartWarmup(); - } else { - ServerCommand("mp_warmup_pausetimer 1"); - ServerCommand("mp_do_warmup_period 1"); - ServerCommand("mp_warmup_pausetimer 1"); - } -} - -stock void StartWarmup(bool indefiniteWarmup = true, int warmupTime = 60) { +stock void StartWarmup(int warmupTime = 0) { ServerCommand("mp_do_warmup_period 1"); - ServerCommand("mp_warmuptime %d", warmupTime); - ServerCommand("mp_warmup_start"); - - // For some reason it needs to get sent twice. Ask Valve. - if (indefiniteWarmup) { - ServerCommand("mp_warmup_pausetimer 1"); + ServerCommand("mp_warmuptime_all_players_connected 0"); + if (!InWarmup()) { + ServerCommand("mp_warmup_start"); + } + if (warmupTime < 1) { + LogDebug("Setting indefinite warmup."); + // Setting mp_warmuptime to anything less than 7 triggers the countdown to restart regardless of + // mp_warmup_pausetimer 1, and this might be tick-related, so we set it to 10 just for good + // measure. + ServerCommand("mp_warmuptime 10"); ServerCommand("mp_warmup_pausetimer 1"); + } else { + ServerCommand("mp_warmuptime %d", warmupTime); + ServerCommand("mp_warmup_pausetimer 0"); } } @@ -284,8 +195,8 @@ stock void EndWarmup(int time = 0) { if (time == 0) { ServerCommand("mp_warmup_end"); } else { - ServerCommand("mp_warmup_pausetimer 0"); ServerCommand("mp_warmuptime %d", time); + ServerCommand("mp_warmup_pausetimer 0"); } } @@ -293,19 +204,10 @@ stock bool IsPaused() { return GameRules_GetProp("m_bMatchWaitingForResume") != 0; } -stock void RestartGame(int delay) { +stock void RestartGame(int delay = 1) { ServerCommand("mp_restartgame %d", delay); } -stock bool IsClientCoaching(int client) { - return GetClientTeam(client) == CS_TEAM_SPECTATOR && - GetEntProp(client, Prop_Send, "m_iCoachingTeam") != 0; -} - -stock void UpdateCoachTarget(int client, int csTeam) { - SetEntProp(client, Prop_Send, "m_iCoachingTeam", csTeam); -} - stock void SetTeamInfo(int csTeam, const char[] name, const char[] flag = "", const char[] logo = "", const char[] matchstat = "", int series_score = 0) { int team_int = (csTeam == CS_TEAM_CT) ? 1 : 2; @@ -328,9 +230,9 @@ stock void SetTeamInfo(int csTeam, const char[] name, const char[] flag = "", !g_DoingBackupRestoreNow) { Get5Team matchTeam = CSTeamToGet5Team(csTeam); if (IsTeamReady(matchTeam)) { - Format(taggedName, sizeof(taggedName), "%T %s", "ReadyTag", LANG_SERVER, name); + Format(taggedName, sizeof(taggedName), "%s %T", name, "ReadyTag", LANG_SERVER); } else { - Format(taggedName, sizeof(taggedName), "%T %s", "NotReadyTag", LANG_SERVER, name); + Format(taggedName, sizeof(taggedName), "%s %T", name, "NotReadyTag", LANG_SERVER); } } else { strcopy(taggedName, sizeof(taggedName), name); @@ -398,7 +300,8 @@ stock int GetCvarIntSafe(const char[] cvarName) { } } -stock void FormatMapName(const char[] mapName, char[] buffer, int len, bool cleanName = false) { +stock void FormatMapName(const char[] mapName, char[] buffer, int len, bool cleanName = false, + bool color = false) { // explode map by '/' so we can remove any directory prefixes (e.g. workshop stuff) char buffers[4][PLATFORM_MAX_PATH]; int numSplits = ExplodeString(mapName, "/", buffers, sizeof(buffers), PLATFORM_MAX_PATH); @@ -431,8 +334,19 @@ stock void FormatMapName(const char[] mapName, char[] buffer, int len, bool clea strcopy(buffer, len, "Vertigo"); } else if (StrEqual(buffer, "de_ancient")) { strcopy(buffer, len, "Ancient"); + } else if (StrEqual(buffer, "de_tuscan")) { + strcopy(buffer, len, "Tuscan"); + } else if (StrEqual(buffer, "de_prime")) { + strcopy(buffer, len, "Prime"); + } else if (StrEqual(buffer, "de_grind")) { + strcopy(buffer, len, "Grind"); + } else if (StrEqual(buffer, "de_mocha")) { + strcopy(buffer, len, "Mocha"); } } + if (color) { + Format(buffer, len, "{GREEN}%s{NORMAL}", buffer); + } } stock void GetCleanMapName(char[] buffer, int size) { @@ -473,28 +387,27 @@ stock int AddKeysToList(KeyValues kv, ArrayList list, int maxKeyLength) { return count; } -stock int AddSubsectionAuthsToList(KeyValues kv, const char[] section, ArrayList list, - int maxKeyLength) { +stock int AddSubsectionAuthsToList(KeyValues kv, const char[] section, ArrayList list) { int count = 0; if (kv.JumpToKey(section)) { - count = AddAuthsToList(kv, list, maxKeyLength); + count = AddAuthsToList(kv, list); kv.GoBack(); } return count; } -stock int AddAuthsToList(KeyValues kv, ArrayList list, int maxKeyLength) { +stock int AddAuthsToList(KeyValues kv, ArrayList list) { int count = 0; - char[] buffer = new char[maxKeyLength]; + char buffer[AUTH_LENGTH]; char steam64[AUTH_LENGTH]; char name[MAX_NAME_LENGTH]; if (kv.GotoFirstSubKey(false)) { do { - kv.GetSectionName(buffer, maxKeyLength); - kv.GetString(NULL_STRING, name, sizeof(name)); + kv.GetSectionName(buffer, AUTH_LENGTH); + ReadEmptyStringInsteadOfPlaceholder(kv, name, sizeof(name)); if (ConvertAuthToSteam64(buffer, steam64)) { list.PushString(steam64); - Get5_SetPlayerName(steam64, name); + Get5_SetPlayerName(steam64, name, true); count++; } } while (kv.GotoNextKey(false)); @@ -512,6 +425,28 @@ stock bool RemoveStringFromArray(ArrayList list, const char[] str) { return false; } +// Because KeyValue cannot write empty strings, we use this to consistently read empty strings and +// replace our empty-string-placeholder with actual empty string. +stock bool ReadEmptyStringInsteadOfPlaceholder(const KeyValues kv, char[] buffer, + const int bufferSize) { + kv.GetString(NULL_STRING, buffer, bufferSize); + if (StrEqual(KEYVALUE_STRING_PLACEHOLDER, buffer)) { + Format(buffer, bufferSize, ""); + return true; + } + return false; +} + +stock bool WritePlaceholderInsteadOfEmptyString(const KeyValues kv, char[] buffer, + const int bufferSize) { + kv.GetString(NULL_STRING, buffer, bufferSize); + if (StrEqual("", buffer)) { + kv.SetString(NULL_STRING, KEYVALUE_STRING_PLACEHOLDER); + return true; + } + return false; +} + stock int OtherCSTeam(int team) { if (team == CS_TEAM_CT) { return CS_TEAM_T; @@ -536,7 +471,7 @@ stock bool IsPlayerTeam(Get5Team team) { return team == Get5Team_1 || team == Get5Team_2; } -public Get5Team VetoFirstFromString(const char[] str) { +stock Get5Team VetoFirstFromString(const char[] str) { if (StrEqual(str, "random", false)) { return view_as(GetRandomInt(0, 1)); } else if (StrEqual(str, "team2", false)) { @@ -560,7 +495,7 @@ stock bool GetAuth(int client, char[] auth, int size) { // TODO: might want a auth->client adt-trie to speed this up, maintained during // client auth and disconnect forwards. stock int AuthToClient(const char[] auth) { - for (int i = 1; i <= MaxClients; i++) { + LOOP_CLIENTS(i) { if (IsAuthedPlayer(i)) { char clientAuth[AUTH_LENGTH]; if (GetAuth(i, clientAuth, sizeof(clientAuth)) && StrEqual(auth, clientAuth)) { @@ -571,11 +506,9 @@ stock int AuthToClient(const char[] auth) { return -1; } -stock int MaxMapsToPlay(int mapsToWin) { - if (g_BO2Match) - return 2; - else - return 2 * mapsToWin - 1; +stock int MapsToWin(int numberOfMaps) { + // This works because integers are rounded down; so 3 / 2 = 1.5, which becomes 1 as integer. + return (numberOfMaps / 2) + 1; } stock void CSTeamString(int csTeam, char[] buffer, int len) { @@ -600,30 +533,7 @@ stock void GetTeamString(Get5Team team, char[] buffer, int len) { } } -stock void GameStateString(Get5State state, char[] buffer, int length) { - switch (state) { - case Get5State_None: - Format(buffer, length, "none"); - case Get5State_PreVeto: - Format(buffer, length, "pre_veto"); - case Get5State_Veto: - Format(buffer, length, "veto"); - case Get5State_Warmup: - Format(buffer, length, "warmup"); - case Get5State_KnifeRound: - Format(buffer, length, "knife"); - case Get5State_WaitingForKnifeRoundDecision: - Format(buffer, length, "waiting_for_knife_decision"); - case Get5State_GoingLive: - Format(buffer, length, "going_live"); - case Get5State_Live: - Format(buffer, length, "live"); - case Get5State_PostGame: - Format(buffer, length, "post_game"); - } -} - -public MatchSideType MatchSideTypeFromString(const char[] str) { +stock MatchSideType MatchSideTypeFromString(const char[] str) { if (StrEqual(str, "normal", false) || StrEqual(str, "standard", false)) { return MatchSideType_Standard; } else if (StrEqual(str, "never_knife", false)) { @@ -633,7 +543,7 @@ public MatchSideType MatchSideTypeFromString(const char[] str) { } } -public void MatchSideTypeToString(MatchSideType type, char[] str, int len) { +stock void MatchSideTypeToString(MatchSideType type, char[] str, int len) { if (type == MatchSideType_Standard) { Format(str, len, "standard"); } else if (type == MatchSideType_NeverKnife) { @@ -643,12 +553,6 @@ public void MatchSideTypeToString(MatchSideType type, char[] str, int len) { } } -stock void ExecCfg(ConVar cvar) { - char cfg[PLATFORM_MAX_PATH]; - cvar.GetString(cfg, sizeof(cfg)); - ServerCommand("exec \"%s\"", cfg); -} - // Taken from Zephyrus (https://forums.alliedmods.net/showpost.php?p=2231850&postcount=2) stock bool ConvertSteam2ToSteam64(const char[] steam2Auth, char[] steam64Auth, int size) { if (strlen(steam2Auth) < 11 || steam2Auth[0] != 'S' || steam2Auth[6] == 'I') { @@ -733,14 +637,10 @@ stock bool HelpfulAttack(int attacker, int victim) { } stock SideChoice SideTypeFromString(const char[] input) { - if (StrEqual(input, "team1_ct", false)) { + if (StrEqual(input, "team1_ct", false) || StrEqual(input, "team2_t", false)) { return SideChoice_Team1CT; - } else if (StrEqual(input, "team1_t", false)) { + } else if (StrEqual(input, "team1_t", false) || StrEqual(input, "team2_ct", false)) { return SideChoice_Team1T; - } else if (StrEqual(input, "team2_ct", false)) { - return SideChoice_Team1T; - } else if (StrEqual(input, "team2_t", false)) { - return SideChoice_Team1CT; } else if (StrEqual(input, "knife", false)) { return SideChoice_KnifeRound; } else { @@ -749,25 +649,9 @@ stock SideChoice SideTypeFromString(const char[] input) { } } -typedef VoidFunction = function void(); - -stock void DelayFunction(float delay, VoidFunction f) { - DataPack p = CreateDataPack(); - p.WriteFunction(f); - CreateTimer(delay, _DelayFunctionCallback, p); -} - -public Action _DelayFunctionCallback(Handle timer, DataPack data) { - data.Reset(); - Function func = data.ReadFunction(); - Call_StartFunction(INVALID_HANDLE, func); - Call_Finish(); - delete data; -} - // Deletes a file if it exists. Returns true if the // file existed AND there was an error deleting it. -public bool DeleteFileIfExists(const char[] path) { +stock bool DeleteFileIfExists(const char[] path) { if (FileExists(path)) { if (!DeleteFile(path)) { LogError("Failed to delete file %s", path); @@ -778,7 +662,7 @@ public bool DeleteFileIfExists(const char[] path) { return true; } -public bool IsJSONPath(const char[] path) { +stock bool IsJSONPath(const char[] path) { int length = strlen(path); if (length >= 5) { return strcmp(path[length - 5], ".json", false) == 0; @@ -787,11 +671,11 @@ public bool IsJSONPath(const char[] path) { } } -public int GetMilliSecondsPassedSince(float timestamp) { +stock int GetMilliSecondsPassedSince(float timestamp) { return RoundToFloor((GetEngineTime() - timestamp) * 1000); } -public int GetRoundsPlayed() { +stock int GetRoundsPlayed() { return GameRules_GetProp("m_totalRoundsPlayed"); } @@ -813,12 +697,13 @@ stock Get5BombSite GetNearestBombsite(int client) { float aDist = GetVectorDistance(aCenter, pos, true); float bDist = GetVectorDistance(bCenter, pos, true); - LogDebug("Bomb planted. Distance to A: %d. Distance to B: %d.", aDist, bDist); + LogDebug("Bomb planted. Distance to A: %f. Distance to B: %f.", aDist, bDist); return (aDist < bDist) ? Get5BombSite_A : Get5BombSite_B; } -stock void convertSecondsToMinutesAndSeconds(int timeAsSeconds, char[] buffer, const int bufferSize) { +stock void convertSecondsToMinutesAndSeconds(int timeAsSeconds, char[] buffer, + const int bufferSize) { int minutes = 0; int seconds = timeAsSeconds; if (timeAsSeconds >= 60) { @@ -827,3 +712,7 @@ stock void convertSecondsToMinutesAndSeconds(int timeAsSeconds, char[] buffer, c } Format(buffer, bufferSize, seconds < 10 ? "%d:0%d" : "%d:%d", minutes, seconds); } + +stock bool IsDoingRestoreOrMapChange() { + return g_DoingBackupRestoreNow || g_WaitingForRoundBackup || g_MapChangePending; +} diff --git a/scripting/get5/version.sp b/scripting/get5/version.sp index 73a92935b..8b9e2220a 100644 --- a/scripting/get5/version.sp +++ b/scripting/get5/version.sp @@ -1,6 +1,6 @@ #tryinclude "manual_version.sp" #if !defined PLUGIN_VERSION -#define PLUGIN_VERSION "0.9.0-dev" +#define PLUGIN_VERSION "0.10.0-dev" #endif // This MUST be the latest version in x.y.z semver format followed by -dev. diff --git a/scripting/get5_apistats.sp b/scripting/get5_apistats.sp index 6ec2b3132..a3f5522f3 100644 --- a/scripting/get5_apistats.sp +++ b/scripting/get5_apistats.sp @@ -72,7 +72,7 @@ public void OnPluginStart() { RegConsoleCmd("get5_web_available", Command_Available); } -public Action Command_Available(int client, int args) { +static Action Command_Available(int client, int args) { char versionString[64] = "unknown"; ConVar versionCvar = FindConVar("get5_version"); if (versionCvar != null) { @@ -94,11 +94,11 @@ public Action Command_Available(int client, int args) { return Plugin_Handled; } -public void LogoBasePathChanged(ConVar convar, const char[] oldValue, const char[] newValue) { +void LogoBasePathChanged(ConVar convar, const char[] oldValue, const char[] newValue) { g_LogoBasePath = g_UseSVGCvar.BoolValue ? LOGO_DIR : LEGACY_LOGO_DIR; } -public void ApiInfoChanged(ConVar convar, const char[] oldValue, const char[] newValue) { +void ApiInfoChanged(ConVar convar, const char[] oldValue, const char[] newValue) { g_APIKeyCvar.GetString(g_APIKey, sizeof(g_APIKey)); g_APIURLCvar.GetString(g_APIURL, sizeof(g_APIURL)); @@ -136,7 +136,7 @@ static Handle CreateRequest(EHTTPMethod httpMethod, const char[] apiMethod, any: } } -public int RequestCallback(Handle request, bool failure, bool requestSuccessful, +int RequestCallback(Handle request, bool failure, bool requestSuccessful, EHTTPStatusCode statusCode) { if (failure || !requestSuccessful) { LogError("API request failed, HTTP status code = %d", statusCode); @@ -163,7 +163,7 @@ public void Get5_OnSeriesInit(const Get5SeriesStartedEvent event) { CheckForLogo(logo2); } -public void CheckForLogo(const char[] logo) { +static void CheckForLogo(const char[] logo) { if (StrEqual(logo, "")) { return; } @@ -196,7 +196,8 @@ public void CheckForLogo(const char[] logo) { } } -public int LogoCallback(Handle request, bool failure, bool successful, EHTTPStatusCode status, int data) { +static int LogoCallback(Handle request, bool failure, bool successful, EHTTPStatusCode status, + int data) { if (failure || !successful) { LogError("Logo request failed, status code = %d", status); return; @@ -235,7 +236,7 @@ public void Get5_OnGoingLive(const Get5GoingLiveEvent event) { Get5_AddLiveCvar("get5_web_api_url", g_APIURL); } -public void UpdateRoundStats(const char[] matchId, int mapNumber) { +static void UpdateRoundStats(const char[] matchId, const int mapNumber) { int t1score = CS_GetTeamScore(Get5_Get5TeamToCSTeam(Get5Team_1)); int t2score = CS_GetTeamScore(Get5_Get5TeamToCSTeam(Get5Team_2)); @@ -252,11 +253,11 @@ public void UpdateRoundStats(const char[] matchId, int mapNumber) { Format(mapKey, sizeof(mapKey), "map%d", mapNumber); if (kv.JumpToKey(mapKey)) { if (kv.JumpToKey("team1")) { - UpdatePlayerStats(matchId, kv, Get5Team_1); + UpdatePlayerStats(matchId, mapNumber, kv, Get5Team_1); kv.GoBack(); } if (kv.JumpToKey("team2")) { - UpdatePlayerStats(matchId, kv, Get5Team_2); + UpdatePlayerStats(matchId, mapNumber, kv, Get5Team_2); kv.GoBack(); } kv.GoBack(); @@ -284,10 +285,10 @@ static void AddIntStat(Handle req, KeyValues kv, const char[] field) { AddIntParam(req, field, kv.GetNum(field)); } -public void UpdatePlayerStats(const char[] matchId, KeyValues kv, Get5Team team) { +static void UpdatePlayerStats(const char[] matchId, const int mapNumber, const KeyValues kv, + const Get5Team team) { char name[MAX_NAME_LENGTH]; char auth[AUTH_LENGTH]; - int mapNumber = Get5_GetMapNumber(); if (kv.GotoFirstSubKey()) { do { @@ -300,7 +301,7 @@ public void UpdatePlayerStats(const char[] matchId, KeyValues kv, Get5Team team) mapNumber, auth); if (req != INVALID_HANDLE) { AddStringParam(req, "team", teamString); - AddStringParam(req, "name", name); + AddStringParam(req, STAT_NAME, name); AddIntStat(req, kv, STAT_KILLS); AddIntStat(req, kv, STAT_DEATHS); AddIntStat(req, kv, STAT_ASSISTS); @@ -308,6 +309,10 @@ public void UpdatePlayerStats(const char[] matchId, KeyValues kv, Get5Team team) AddIntStat(req, kv, STAT_TEAMKILLS); AddIntStat(req, kv, STAT_SUICIDES); AddIntStat(req, kv, STAT_DAMAGE); + AddIntStat(req, kv, STAT_UTILITY_DAMAGE); + AddIntStat(req, kv, STAT_ENEMIES_FLASHED); + AddIntStat(req, kv, STAT_FRIENDLIES_FLASHED); + AddIntStat(req, kv, STAT_KNIFE_KILLS); AddIntStat(req, kv, STAT_HEADSHOT_KILLS); AddIntStat(req, kv, STAT_ROUNDSPLAYED); AddIntStat(req, kv, STAT_BOMBPLANTS); @@ -329,6 +334,7 @@ public void UpdatePlayerStats(const char[] matchId, KeyValues kv, Get5Team team) AddIntStat(req, kv, STAT_TRADEKILL); AddIntStat(req, kv, STAT_KAST); AddIntStat(req, kv, STAT_CONTRIBUTION_SCORE); + AddIntStat(req, kv, STAT_MVP); SteamWorks_SendHTTPRequest(req); } @@ -377,6 +383,6 @@ public void Get5_OnRoundStatsUpdated(const Get5RoundStatsUpdatedEvent event) { if (Get5_GetGameState() == Get5State_Live) { char matchId[64]; event.GetMatchId(matchId, sizeof(matchId)); - UpdateRoundStats(matchId, Get5_GetMapNumber()); + UpdateRoundStats(matchId, event.MapNumber); } } diff --git a/scripting/get5_mysqlstats.sp b/scripting/get5_mysqlstats.sp index 03c970c91..ddd063eb0 100644 --- a/scripting/get5_mysqlstats.sp +++ b/scripting/get5_mysqlstats.sp @@ -115,7 +115,7 @@ public void Get5_OnSeriesInit(const Get5SeriesStartedEvent event) { } } -public void MatchInitCallback(Database dbObj, DBResultSet results, const char[] error, any data) { +static void MatchInitCallback(Database dbObj, DBResultSet results, const char[] error, any data) { if (results == null) { LogError("Failed to get Match ID from match init query: %s.", error); g_DisableStats = true; @@ -157,7 +157,7 @@ public void Get5_OnGoingLive(const Get5GoingLiveEvent event) { db.Query(SQLErrorCheckCallback, queryBuffer); } -public void UpdateRoundStats(const char[] matchId, int mapNumber) { +static void UpdateRoundStats(const char[] matchId, const int mapNumber) { // Update team scores int t1score = CS_GetTeamScore(Get5_Get5TeamToCSTeam(Get5Team_1)); int t2score = CS_GetTeamScore(Get5_Get5TeamToCSTeam(Get5Team_2)); @@ -178,11 +178,11 @@ public void UpdateRoundStats(const char[] matchId, int mapNumber) { Format(mapKey, sizeof(mapKey), "map%d", mapNumber); if (kv.JumpToKey(mapKey)) { if (kv.JumpToKey("team1")) { - AddPlayerStats(matchId, kv, Get5Team_1); + AddPlayerStats(matchId, mapNumber, kv, Get5Team_1); kv.GoBack(); } if (kv.JumpToKey("team2")) { - AddPlayerStats(matchId, kv, Get5Team_2); + AddPlayerStats(matchId, mapNumber, kv, Get5Team_2); kv.GoBack(); } kv.GoBack(); @@ -223,18 +223,21 @@ public void Get5_OnMapResult(const Get5MapResultEvent event) { db.Query(SQLErrorCheckCallback, queryBuffer); } -public void AddPlayerStats(const char[] matchId, KeyValues kv, Get5Team team) { +static void AddPlayerStats(const char[] matchId, const int mapNumber, const KeyValues kv, + const Get5Team team) { char name[MAX_NAME_LENGTH]; char auth[AUTH_LENGTH]; char nameSz[MAX_NAME_LENGTH * 2 + 1]; char authSz[AUTH_LENGTH * 2 + 1]; - int mapNumber = Get5_GetMapNumber(); char matchIdSz[64]; db.Escape(matchId, matchIdSz, sizeof(matchIdSz)); if (kv.GotoFirstSubKey()) { do { + if (kv.GetNum(STAT_COACHING, 0) > 0) { + continue; // Don't update stats for coaches. + } kv.GetSectionName(auth, sizeof(auth)); kv.GetString("name", name, sizeof(name)); db.Escape(auth, authSz, sizeof(authSz)); @@ -371,7 +374,7 @@ public void Get5_OnSeriesResult(const Get5SeriesResultEvent event) { db.Query(SQLErrorCheckCallback, queryBuffer); } -public int SQLErrorCheckCallback(Handle owner, Handle hndl, const char[] error, int data) { +static int SQLErrorCheckCallback(Handle owner, Handle hndl, const char[] error, int data) { if (!StrEqual("", error)) { LogError("Last Connect SQL Error: %s", error); } @@ -381,6 +384,6 @@ public void Get5_OnRoundStatsUpdated(const Get5RoundStatsUpdatedEvent event) { if (Get5_GetGameState() == Get5State_Live && !g_DisableStats) { char matchId[64]; event.GetMatchId(matchId, sizeof(matchId)); - UpdateRoundStats(matchId, Get5_GetMapNumber()); + UpdateRoundStats(matchId, event.MapNumber); } } diff --git a/scripting/include/get5.inc b/scripting/include/get5.inc index d582bcd6d..e4672aa11 100644 --- a/scripting/include/get5.inc +++ b/scripting/include/get5.inc @@ -87,8 +87,9 @@ native bool Get5_LoadMatchConfigFromURL(const char[] url, ArrayList paramNames = native bool Get5_AddPlayerToTeam(const char[] steamId, Get5Team team, const char[] playerName = ""); -// Force sets a steam64 to map to a specified playername -native bool Get5_SetPlayerName(const char[] steamId, const char[] playerName); +// Force sets a steam64 to map to a specified playername. If calling this multiple times, you may want to +// suppress loading the player names until the last call. +native bool Get5_SetPlayerName(const char[] steamId, const char[] playerName, bool suppressNameLoading = false); // Removes a player from all match teams. // Returns if they were successfully removed (false if not round). @@ -135,254 +136,251 @@ native int Get5_IncreasePlayerStat(int client, const char[] statName, int amount methodmap Get5StatusTeam < JSON_Object { - public bool SetTeamName(const char[] event) { - return this.SetString("name", event); - } - - property int SeriesScore { - public set(int score) { - this.SetInt("series_score", score); - } - } + public bool SetTeamName(const char[] event) { + return this.SetString("name", event); + } - property int MapScore { - public set(int score) { - this.SetInt("current_map_score", score); - } + property int SeriesScore { + public set(int score) { + this.SetInt("series_score", score); } + } - property int ConnectedClients { - public set(int clients) { - this.SetInt("connected_clients", clients); - } + property int MapScore { + public set(int score) { + this.SetInt("current_map_score", score); } + } - property bool Ready { - public set(bool ready) { - this.SetBool("ready", ready); - } + property int ConnectedClients { + public set(int clients) { + this.SetInt("connected_clients", clients); } + } - property Get5Side Side { - public set(Get5Side side) { - this.SetInt("side_int", view_as(side)); - this.SetHidden("side_int", true); - ConvertGet5SideToStringInJson(this, "side", side); - } + property bool Ready { + public set(bool ready) { + this.SetBool("ready", ready); } + } - public Get5StatusTeam(const char[] teamName, - const int seriesScore, const int mapScore, const bool ready, const Get5Side side, const int connectedClients) { - Get5StatusTeam self = view_as(new JSON_Object()); - self.SetTeamName(teamName); - self.SeriesScore = seriesScore; - self.MapScore = mapScore; - self.Ready = ready; - self.Side = side; - self.ConnectedClients = connectedClients; - return self; + property Get5Side Side { + public set(Get5Side side) { + this.SetInt("side_int", view_as(side)); + this.SetHidden("side_int", true); + ConvertGet5SideToStringInJson(this, "side", side); } + } + public Get5StatusTeam(const char[] teamName, + const int seriesScore, const int mapScore, const bool ready, const Get5Side side, const int connectedClients) { + Get5StatusTeam self = view_as(new JSON_Object()); + self.SetTeamName(teamName); + self.SeriesScore = seriesScore; + self.MapScore = mapScore; + self.Ready = ready; + self.Side = side; + self.ConnectedClients = connectedClients; + return self; + } } methodmap Get5Status < JSON_Object { - public bool SetPluginVersion(const char[] event) { - return this.SetString("plugin_version", event); - } + public bool SetPluginVersion(const char[] event) { + return this.SetString("plugin_version", event); + } - property Get5State GameState { - public set(Get5State state) { - ConvertGameStateToStringInJson(this, "gamestate", state); - } + property Get5State GameState { + public set(Get5State state) { + ConvertGameStateToStringInJson(this, "gamestate", state); } + } - property bool IsPaused { - public set(bool paused) { - this.SetBool("paused", paused); - } + property bool IsPaused { + public set(bool paused) { + this.SetBool("paused", paused); } + } - public bool SetConfigFile(const char[] file) { - return this.SetString("loaded_config_file", file); - } + public bool SetConfigFile(const char[] file) { + return this.SetString("loaded_config_file", file); + } - public bool SetMatchId(const char[] matchId) { - return this.SetString("matchid", matchId); - } + public bool SetMatchId(const char[] matchId) { + return this.SetString("matchid", matchId); + } - property int MapNumber { - public set(int mapNumber) { - this.SetInt("map_number", mapNumber); - } + property int MapNumber { + public set(int mapNumber) { + this.SetInt("map_number", mapNumber); } + } - property int RoundNumber { - public set(int roundNumber) { - this.SetInt("round_number", roundNumber); - } + property int RoundNumber { + public set(int roundNumber) { + this.SetInt("round_number", roundNumber); } + } - property int RoundTime { - public set(int roundTime) { - this.SetInt("round_time", roundTime); - } + property int RoundTime { + public set(int roundTime) { + this.SetInt("round_time", roundTime); } + } - property Get5StatusTeam Team1 { - public set(Get5StatusTeam team) { - this.SetObject("team1", team); - } + property Get5StatusTeam Team1 { + public set(Get5StatusTeam team) { + this.SetObject("team1", team); } + } - property Get5StatusTeam Team2 { - public set(Get5StatusTeam team) { - this.SetObject("team2", team); - } + property Get5StatusTeam Team2 { + public set(Get5StatusTeam team) { + this.SetObject("team2", team); } + } - public bool AddMap(const char[] map) { - if (!this.HasKey("maps")) { - this.SetObject("maps", new JSON_Array()); - } - JSON_Array maps = view_as(this.GetObject("maps")); - maps.PushString(map); + public bool AddMap(const char[] map) { + if (!this.HasKey("maps")) { + this.SetObject("maps", new JSON_Array()); } + JSON_Array maps = view_as(this.GetObject("maps")); + maps.PushString(map); + } - public Get5Status(const char[] pluginVersion, const Get5State gamestate, const bool isPaused) { - Get5Status self = view_as(new JSON_Object()); - self.SetPluginVersion(pluginVersion); - self.GameState = gamestate; - self.IsPaused = isPaused; - return self; - } + public Get5Status(const char[] pluginVersion, const Get5State gamestate, const bool isPaused) { + Get5Status self = view_as(new JSON_Object()); + self.SetPluginVersion(pluginVersion); + self.GameState = gamestate; + self.IsPaused = isPaused; + return self; + } } methodmap Get5Weapon < JSON_Object { - public bool SetWeaponName(const char[] value) { - return this.SetString("name", value); - } + public bool SetWeaponName(const char[] value) { + return this.SetString("name", value); + } - public bool GetWeaponName(char[] buffer, const int maxSize) { - return this.GetString("name", buffer, maxSize); - } + public bool GetWeaponName(char[] buffer, const int maxSize) { + return this.GetString("name", buffer, maxSize); + } - property CSWeaponID Id { - public get() { - return view_as(this.GetInt("id")); - } - public set(CSWeaponID id) { - this.SetInt("id", view_as(id)); - } + property CSWeaponID Id { + public get() { + return view_as(this.GetInt("id")); } - - public Get5Weapon(const char[] weapon, CSWeaponID weaponId) { - Get5Weapon self = view_as(new JSON_Object()); - self.SetWeaponName(weapon); - self.Id = weaponId; - return self; + public set(CSWeaponID id) { + this.SetInt("id", view_as(id)); } + } + + public Get5Weapon(const char[] weapon, CSWeaponID weaponId) { + Get5Weapon self = view_as(new JSON_Object()); + self.SetWeaponName(weapon); + self.Id = weaponId; + return self; + } } methodmap Get5Winner < JSON_Object { - property Get5Side Side { - public get() { - return view_as(this.GetInt("side_int")); - } - public set(Get5Side side) { - this.SetInt("side_int", view_as(side)); - this.SetHidden("side_int", true); - ConvertGet5SideToStringInJson(this, "side", side); - } + property Get5Side Side { + public get() { + return view_as(this.GetInt("side_int")); } - - property Get5Team Team { - public get() { - return view_as(this.GetInt("team_int")); - } - public set(Get5Team team) { - this.SetInt("team_int", view_as(team)); - this.SetHidden("team_int", true); - ConvertGet5TeamToStringInJson(this, "team", team); - } + public set(Get5Side side) { + this.SetInt("side_int", view_as(side)); + this.SetHidden("side_int", true); + ConvertGet5SideToStringInJson(this, "side", side); } + } - public Get5Winner(Get5Team team, Get5Side side) { - Get5Winner self = view_as(new JSON_Object()); - self.Team = team; - self.Side = side; - return self; + property Get5Team Team { + public get() { + return view_as(this.GetInt("team_int")); + } + public set(Get5Team team) { + this.SetInt("team_int", view_as(team)); + this.SetHidden("team_int", true); + ConvertGet5TeamToStringInJson(this, "team", team); } + } + + public Get5Winner(Get5Team team, Get5Side side) { + Get5Winner self = view_as(new JSON_Object()); + self.Team = team; + self.Side = side; + return self; + } } methodmap Get5Player < JSON_Object { - property Get5Side Side { - public get() { - return view_as(this.GetInt("side_int")); - } - public set(Get5Side side) { - this.SetInt("side_int", view_as(side)); - this.SetHidden("side_int", true); - ConvertGet5SideToStringInJson(this, "side", side); - } + property Get5Side Side { + public get() { + return view_as(this.GetInt("side_int")); } - - public bool SetSteamId(const char[] value) { - return this.SetString("steamid", value); + public set(Get5Side side) { + this.SetInt("side_int", view_as(side)); + this.SetHidden("side_int", true); + ConvertGet5SideToStringInJson(this, "side", side); } + } - public bool GetSteamId(char[] buffer, const int maxSize) { - return this.GetString("steamid", buffer, maxSize); - } + public bool SetSteamId(const char[] value) { + return this.SetString("steamid", value); + } + public bool GetSteamId(char[] buffer, const int maxSize) { + return this.GetString("steamid", buffer, maxSize); + } - public bool SetName(const char[] value) { - return this.SetString("name", value); - } + public bool SetName(const char[] value) { + return this.SetString("name", value); + } + public bool GetName(char[] buffer, const int maxSize) { + return this.GetString("name", buffer, maxSize); + } - public bool GetName(char[] buffer, const int maxSize) { - return this.GetString("name", buffer, maxSize); + property bool IsBot { + public get() { + return this.GetBool("is_bot"); } - - property bool IsBot { - public get() { - return this.GetBool("is_bot"); - } - public set(bool bot) { - this.SetBool("is_bot", bot); - } + public set(bool bot) { + this.SetBool("is_bot", bot); } + } - property int UserId { - public get() { - return this.GetInt("user_id"); - } - public set(int id) { - this.SetInt("user_id", id); - } + property int UserId { + public get() { + return this.GetInt("user_id"); } - - public Get5Player(const int userId, const char[] steamId, const Get5Side side, const char[] name, const bool isBot) { - Get5Player self = view_as(new JSON_Object()); - self.UserId = userId; - self.SetSteamId(steamId); - self.Side = side; - self.SetName(name); - self.IsBot = isBot; - return self; + public set(int id) { + this.SetInt("user_id", id); } + } + + public Get5Player(const int userId, const char[] steamId, const Get5Side side, const char[] name, const bool isBot) { + Get5Player self = view_as(new JSON_Object()); + self.UserId = userId; + self.SetSteamId(steamId); + self.Side = side; + self.SetName(name); + self.IsBot = isBot; + return self; + } } methodmap Get5Event < JSON_Object { - public bool SetEvent(const char[] event) { - return this.SetString("event", event); - } - public bool GetEvent(char[] buffer, const int maxSize) { - return this.GetString("event", buffer, maxSize); - } + public bool SetEvent(const char[] event) { + return this.SetString("event", event); + } + public bool GetEvent(char[] buffer, const int maxSize) { + return this.GetString("event", buffer, maxSize); + } } methodmap Get5PlayerConnectedEvent < Get5Event { @@ -391,7 +389,6 @@ methodmap Get5PlayerConnectedEvent < Get5Event { public get() { return view_as(this.GetObject("player")); } - public set(Get5Player player) { this.SetObject("player", player); } @@ -426,15 +423,12 @@ methodmap Get5PlayerDisconnectedEvent < Get5PlayerConnectedEvent { methodmap Get5MatchEvent < Get5Event { - public bool SetMatchId(const char[] matchId) - { - return this.SetString("matchid", matchId); - } - - public bool GetMatchId(char[] buffer, const int maxSize) { - return this.GetString("matchid", buffer, maxSize); - } - + public bool SetMatchId(const char[] matchId) { + return this.SetString("matchid", matchId); + } + public bool GetMatchId(char[] buffer, const int maxSize) { + return this.GetString("matchid", buffer, maxSize); + } } methodmap Get5MatchTeamEvent < Get5MatchEvent { @@ -443,7 +437,6 @@ methodmap Get5MatchTeamEvent < Get5MatchEvent { public get() { return view_as(this.GetInt("team_int")); } - public set(Get5Team team) { this.SetInt("team_int", view_as(team)); this.SetHidden("team_int", true); @@ -454,23 +447,21 @@ methodmap Get5MatchTeamEvent < Get5MatchEvent { methodmap Get5MapEvent < Get5MatchEvent { - public bool SetMapName(const char[] map) { - return this.SetString("map_name", map); - } + public bool SetMapName(const char[] map) { + return this.SetString("map_name", map); + } + public bool GetMapName(char[] buffer, const int maxSize) { + return this.GetString("map_name", buffer, maxSize); + } - public bool GetMapName(char[] buffer, const int maxSize) { - return this.GetString("map_name", buffer, maxSize); + property int MapNumber { + public get() { + return this.GetInt("map_number"); } - - property int MapNumber { - public get() { - return this.GetInt("map_number"); - } - - public set(int mapNumber) { - this.SetInt("map_number", mapNumber); - } + public set(int mapNumber) { + this.SetInt("map_number", mapNumber); } + } } methodmap Get5MapTeamEvent < Get5MapEvent { @@ -479,7 +470,6 @@ methodmap Get5MapTeamEvent < Get5MapEvent { public get() { return view_as(this.GetInt("team_int")); } - public set(Get5Team team) { this.SetInt("team_int", view_as(team)); this.SetHidden("team_int", true); @@ -490,67 +480,62 @@ methodmap Get5MapTeamEvent < Get5MapEvent { methodmap Get5RoundEvent < Get5MapEvent { - property int RoundNumber { - public get() { - return this.GetInt("round_number"); - } - - public set(int roundNumber) { - this.SetInt("round_number", roundNumber); - } + property int RoundNumber { + public get() { + return this.GetInt("round_number"); + } + public set(int roundNumber) { + this.SetInt("round_number", roundNumber); } + } } methodmap Get5TimedRoundEvent < Get5RoundEvent { - property int RoundTime { - public get() { - return this.GetInt("round_time"); - } - - public set(int roundTime) { - this.SetInt("round_time", roundTime); - } + property int RoundTime { + public get() { + return this.GetInt("round_time"); + } + public set(int roundTime) { + this.SetInt("round_time", roundTime); } + } } methodmap Get5PlayerMapEvent < Get5MapEvent { - property Get5Player Player { - public get() { - return view_as(this.GetObject("player")); - } - - public set(Get5Player player) { - this.SetObject("player", player); - } + property Get5Player Player { + public get() { + return view_as(this.GetObject("player")); + } + public set(Get5Player player) { + this.SetObject("player", player); } + } } methodmap Get5PlayerRoundEvent < Get5RoundEvent { - property Get5Player Player { - public get() { - return view_as(this.GetObject("player")); - } - - public set(Get5Player player) { - this.SetObject("player", player); - } + property Get5Player Player { + public get() { + return view_as(this.GetObject("player")); + } + public set(Get5Player player) { + this.SetObject("player", player); } + } } methodmap Get5PlayerTimedRoundEvent < Get5TimedRoundEvent { - property Get5Player Player { - public get() { - return view_as(this.GetObject("player")); - } - - public set(Get5Player player) { - this.SetObject("player", player); - } + property Get5Player Player { + public get() { + return view_as(this.GetObject("player")); + } + public set(Get5Player player) { + this.SetObject("player", player); } + } } // MATCH CONFIG @@ -561,30 +546,27 @@ methodmap Get5SeriesResultEvent < Get5MatchEvent { public get() { return view_as(this.GetObject("winner")); } - public set(Get5Winner winner) { this.SetObject("winner", winner); } } property int Team1SeriesScore { - public get() { - return this.GetInt("team1_series_score"); - } - - public set(int score) { - this.SetInt("team1_series_score", score); - } + public get() { + return this.GetInt("team1_series_score"); + } + public set(int score) { + this.SetInt("team1_series_score", score); + } } property int Team2SeriesScore { - public get() { - return this.GetInt("team2_series_score"); - } - - public set(int score) { - this.SetInt("team2_series_score", score); - } + public get() { + return this.GetInt("team2_series_score"); + } + public set(int score) { + this.SetInt("team2_series_score", score); + } } public Get5SeriesResultEvent(const char[] matchId, const Get5Winner winner, const int team1Score, const int team2Score) { @@ -667,13 +649,12 @@ methodmap Get5TeamReadyStatusChangedEvent < Get5MatchTeamEvent { methodmap Get5MapSelectionEvent < Get5MatchTeamEvent { - public bool SetMapName(const char[] map) - { - return this.SetString("map_name", map); - } - public bool GetMapName(char[] buffer, const int maxSize) { - return this.GetString("map_name", buffer, maxSize); - } + public bool SetMapName(const char[] map) { + return this.SetString("map_name", map); + } + public bool GetMapName(char[] buffer, const int maxSize) { + return this.GetString("map_name", buffer, maxSize); + } } methodmap Get5MapPickedEvent < Get5MapSelectionEvent { @@ -833,14 +814,12 @@ methodmap Get5RoundStatsUpdatedEvent < Get5RoundEvent { methodmap Get5DemoFinishedEvent < Get5MapEvent { - public bool SetFileName(const char[] filename) - { - return this.SetString("filename", filename); - } - - public bool GetFileName(char[] buffer, const int maxSize) { - return this.GetString("filename", buffer, maxSize); - } + public bool SetFileName(const char[] filename) { + return this.SetString("filename", filename); + } + public bool GetFileName(char[] buffer, const int maxSize) { + return this.GetString("filename", buffer, maxSize); + } public Get5DemoFinishedEvent(const char[] matchId, const int mapNumber, const char[] filename) { Get5DemoFinishedEvent self = view_as(new JSON_Object()); @@ -850,72 +829,66 @@ methodmap Get5DemoFinishedEvent < Get5MapEvent { self.SetFileName(filename); return self; } - } methodmap Get5KnifeRoundStartedEvent < Get5MapEvent { - public Get5KnifeRoundStartedEvent(const char[] matchId, int mapNumber) { - Get5KnifeRoundStartedEvent self = view_as(new JSON_Object()); - self.SetEvent("knife_start"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - return self; + public Get5KnifeRoundStartedEvent(const char[] matchId, int mapNumber) { + Get5KnifeRoundStartedEvent self = view_as(new JSON_Object()); + self.SetEvent("knife_start"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + return self; } - } methodmap Get5KnifeRoundWonEvent < Get5MapTeamEvent { - // We don't use Get5Winner here as the side represents the picked side. - // Team already represents the winning team. Winning side is irrelevant in knife. - property Get5Side Side { - public get() { - return view_as(this.GetInt("side_int")); - } - - public set(Get5Side side) { - this.SetInt("side_int", view_as(side)); - this.SetHidden("side_int", true); - ConvertGet5SideToStringInJson(this, "side", side); - } + // We don't use Get5Winner here as the side represents the picked side. + // Team already represents the winning team. Winning side is irrelevant in knife. + property Get5Side Side { + public get() { + return view_as(this.GetInt("side_int")); } - - property bool Swapped { - public get() { - return this.GetBool("swapped"); - } - - public set(bool swapped) { - this.SetBool("swapped", swapped); - } + public set(Get5Side side) { + this.SetInt("side_int", view_as(side)); + this.SetHidden("side_int", true); + ConvertGet5SideToStringInJson(this, "side", side); } + } - public Get5KnifeRoundWonEvent(const char[] matchId, int mapNumber, const Get5Team winner, const Get5Side side, const bool swapped) { - Get5KnifeRoundWonEvent self = view_as(new JSON_Object()); - self.SetEvent("knife_won"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.Team = winner; - self.Side = side; - self.Swapped = swapped; - return self; + property bool Swapped { + public get() { + return this.GetBool("swapped"); + } + public set(bool swapped) { + this.SetBool("swapped", swapped); + } } + public Get5KnifeRoundWonEvent(const char[] matchId, int mapNumber, const Get5Team winner, const Get5Side side, const bool swapped) { + Get5KnifeRoundWonEvent self = view_as(new JSON_Object()); + self.SetEvent("knife_won"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.Team = winner; + self.Side = side; + self.Swapped = swapped; + return self; + } } methodmap Get5MatchPauseEvent < Get5MapTeamEvent { property Get5PauseType PauseType { - public get() { - return view_as(this.GetInt("pause_type_int")); - } - - public set(Get5PauseType type) { - this.SetInt("pause_type_int", view_as(type)); - this.SetHidden("pause_type_int", true); - ConvertGet5PauseTypeToStringInJson(this, "pause_type", type); - } + public get() { + return view_as(this.GetInt("pause_type_int")); + } + public set(Get5PauseType type) { + this.SetInt("pause_type_int", view_as(type)); + this.SetHidden("pause_type_int", true); + ConvertGet5PauseTypeToStringInJson(this, "pause_type", type); + } } } @@ -930,7 +903,6 @@ methodmap Get5MatchPausedEvent < Get5MatchPauseEvent { self.PauseType = pauseType; return self; } - } methodmap Get5MatchUnpausedEvent < Get5MatchPauseEvent { @@ -944,81 +916,75 @@ methodmap Get5MatchUnpausedEvent < Get5MatchPauseEvent { self.PauseType = pauseType; return self; } - } methodmap Get5SeriesStartedEvent < Get5MatchEvent { - public bool SetTeam1Name(const char[] value) - { - return this.SetString("team1_name", value); - } - - public bool GetTeam1Name(char[] buffer, const int maxSize) { - return this.GetString("team1_name", buffer, maxSize); - } - - public bool SetTeam2Name(const char[] value) - { - return this.SetString("team2_name", value); - } + public bool SetTeam1Name(const char[] value) { + return this.SetString("team1_name", value); + } + public bool GetTeam1Name(char[] buffer, const int maxSize) { + return this.GetString("team1_name", buffer, maxSize); + } - public bool GetTeam2Name(char[] buffer, const int maxSize) { - return this.GetString("team2_name", buffer, maxSize); - } + public bool SetTeam2Name(const char[] value) { + return this.SetString("team2_name", value); + } + public bool GetTeam2Name(char[] buffer, const int maxSize) { + return this.GetString("team2_name", buffer, maxSize); + } public Get5SeriesStartedEvent(const char[] matchId, const char[] team1Name, const char[] team2Name) { - Get5SeriesStartedEvent self = view_as(new JSON_Object()); - self.SetEvent("series_start"); - self.SetMatchId(matchId); - self.SetTeam1Name(team1Name); - self.SetTeam2Name(team2Name); - return self; - } + Get5SeriesStartedEvent self = view_as(new JSON_Object()); + self.SetEvent("series_start"); + self.SetMatchId(matchId); + self.SetTeam1Name(team1Name); + self.SetTeam2Name(team2Name); + return self; + } } -methodmap Get5BackupRestoredEvent < Get5MapEvent { +methodmap Get5BackupRestoredEvent < Get5RoundEvent { - public bool SetFileName(const char[] file) - { - return this.SetString("filename", file); - } - - public bool GetFileName(char[] buffer, const int maxSize) { - return this.GetString("filename", buffer, maxSize); - } + public bool SetFileName(const char[] file) { + return this.SetString("filename", file); + } + public bool GetFileName(char[] buffer, const int maxSize) { + return this.GetString("filename", buffer, maxSize); + } - public Get5BackupRestoredEvent(const char[] matchId, const int mapNumber, const char[] file) { - Get5BackupRestoredEvent self = view_as(new JSON_Object()); - self.SetEvent("backup_loaded"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.SetFileName(file); - return self; - } + public Get5BackupRestoredEvent(const char[] matchId, const int mapNumber, const int roundNumber, const char[] file) { + Get5BackupRestoredEvent self = view_as(new JSON_Object()); + self.SetEvent("backup_loaded"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.SetFileName(file); + return self; + } } methodmap Get5RoundStartedEvent < Get5RoundEvent { public Get5RoundStartedEvent(const char[] matchId, const int mapNumber, const int roundNumber) { - Get5RoundStartedEvent self = view_as(new JSON_Object()); - self.SetEvent("round_start"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - return self; - } + Get5RoundStartedEvent self = view_as(new JSON_Object()); + self.SetEvent("round_start"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + return self; + } } methodmap Get5GoingLiveEvent < Get5MapEvent { public Get5GoingLiveEvent(const char[] matchId, const int mapNumber) { - Get5GoingLiveEvent self = view_as(new JSON_Object()); - self.SetEvent("going_live"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - return self; - } + Get5GoingLiveEvent self = view_as(new JSON_Object()); + self.SetEvent("going_live"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + return self; + } } methodmap Get5RoundEndedEvent < Get5TimedRoundEvent { @@ -1026,152 +992,138 @@ methodmap Get5RoundEndedEvent < Get5TimedRoundEvent { // Note that reason is decremented by 1 to match the values defined at https://github.com/alliedmodders/sourcemod/blob/master/plugins/include/cstrike.inc // CSGO increments these by 1 for some reason. property CSRoundEndReason Reason { - public get() { - return view_as(this.GetInt("reason")); - } - - public set(CSRoundEndReason reason) { - this.SetInt("reason", view_as(reason)); - } + public get() { + return view_as(this.GetInt("reason")); + } + public set(CSRoundEndReason reason) { + this.SetInt("reason", view_as(reason)); + } } property Get5Winner Winner { - public get() { - return view_as(this.GetObject("winner")); - } - - public set(Get5Winner winner) { - this.SetObject("winner", winner); - } + public get() { + return view_as(this.GetObject("winner")); + } + public set(Get5Winner winner) { + this.SetObject("winner", winner); + } } property int Team1Score { - public get() { - return this.GetInt("team1_score"); - } - - public set(int score) { - this.SetInt("team1_score", score); - } + public get() { + return this.GetInt("team1_score"); + } + public set(int score) { + this.SetInt("team1_score", score); + } } property int Team2Score { - public get() { - return this.GetInt("team2_score"); - } - - public set(int score) { - this.SetInt("team2_score", score); - } + public get() { + return this.GetInt("team2_score"); + } + public set(int score) { + this.SetInt("team2_score", score); + } } public Get5RoundEndedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const CSRoundEndReason reason, const Get5Winner winner, const int team1Score, const int team2Score) { - Get5RoundEndedEvent self = view_as(new JSON_Object()); - self.SetEvent("round_end"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.Reason = reason; - self.Winner = winner; - self.Team1Score = team1Score; - self.Team2Score = team2Score; - return self; - } + Get5RoundEndedEvent self = view_as(new JSON_Object()); + self.SetEvent("round_end"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.Reason = reason; + self.Winner = winner; + self.Team1Score = team1Score; + self.Team2Score = team2Score; + return self; + } } // All other events methodmap Get5PlayerSayEvent < Get5PlayerTimedRoundEvent { - public bool SetCommand(const char[] command) - { + public bool SetCommand(const char[] command) { return this.SetString("command", command); } - public bool GetCommand(char[] buffer, const int maxSize) { return this.GetString("command", buffer, maxSize); } - public bool SetMessage(const char[] message) - { + public bool SetMessage(const char[] message) { return this.SetString("message", message); } - public bool GetMessage(char[] buffer, const int maxSize) { return this.GetString("message", buffer, maxSize); } public Get5PlayerSayEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player, const char[] command, const char[] message) { - Get5PlayerSayEvent self = view_as(new JSON_Object()); - self.SetEvent("player_say"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.SetCommand(command); - self.SetMessage(message); - return self; - } + Get5PlayerSayEvent self = view_as(new JSON_Object()); + self.SetEvent("player_say"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.SetCommand(command); + self.SetMessage(message); + return self; + } } methodmap Get5RoundMVPEvent < Get5PlayerRoundEvent { // There doesn't seem to be an enum for MVP reason, so we go with the plain integer. property int Reason { - public get() { - return this.GetInt("reason"); - } - - public set(int reason) { - this.SetInt("reason", reason); - } + public get() { + return this.GetInt("reason"); + } + public set(int reason) { + this.SetInt("reason", reason); + } } public Get5RoundMVPEvent(const char[] matchId, const int mapNumber, const int roundNumber, const Get5Player player, const int reason) { - Get5RoundMVPEvent self = view_as(new JSON_Object()); - self.SetEvent("round_mvp"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.Player = player; - self.Reason = reason; - return self; - } + Get5RoundMVPEvent self = view_as(new JSON_Object()); + self.SetEvent("round_mvp"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.Player = player; + self.Reason = reason; + return self; + } } methodmap Get5AssisterObject < JSON_Object { property Get5Player Player { - public get() { - return view_as(this.GetObject("player")); - } - - public set(Get5Player player) { - this.SetObject("player", player); - } + public get() { + return view_as(this.GetObject("player")); + } + public set(Get5Player player) { + this.SetObject("player", player); + } } property bool FriendlyFire { - public get() { - return this.GetBool("friendly_fire"); - } - - public set(bool friendlyFire) { - this.SetBool("friendly_fire", friendlyFire); - } - + public get() { + return this.GetBool("friendly_fire"); + } + public set(bool friendlyFire) { + this.SetBool("friendly_fire", friendlyFire); + } } property bool FlashAssist { - public get() { - return this.GetBool("flash_assist"); - } - - public set(bool flashAssist) { - this.SetBool("flash_assist", flashAssist); - } - + public get() { + return this.GetBool("flash_assist"); + } + public set(bool flashAssist) { + this.SetBool("flash_assist", flashAssist); + } } public Get5AssisterObject(const Get5Player player, bool flashAssist, bool friendlyFire) { @@ -1181,122 +1133,110 @@ methodmap Get5AssisterObject < JSON_Object { self.FriendlyFire = friendlyFire; return self; } - } methodmap Get5PlayerWeaponEvent < Get5PlayerTimedRoundEvent { - property Get5Weapon Weapon { - public get() { - return view_as(this.GetObject("weapon")); - } - - public set(Get5Weapon weapon) { - this.SetObject("weapon", weapon); - } + property Get5Weapon Weapon { + public get() { + return view_as(this.GetObject("weapon")); + } + public set(Get5Weapon weapon) { + this.SetObject("weapon", weapon); } + } } methodmap Get5PlayerDeathEvent < Get5PlayerWeaponEvent { property bool Bomb { - public get() { - return this.GetBool("bomb"); - } - - public set(bool bomb) { - this.SetBool("bomb", bomb); - } + public get() { + return this.GetBool("bomb"); + } + public set(bool bomb) { + this.SetBool("bomb", bomb); + } } property bool Headshot { - public get() { - return this.GetBool("headshot"); - } - - public set(bool headshot) { - this.SetBool("headshot", headshot); - } + public get() { + return this.GetBool("headshot"); + } + public set(bool headshot) { + this.SetBool("headshot", headshot); + } } property bool ThruSmoke { - public get() { - return this.GetBool("thru_smoke"); - } - - public set(bool thruSmoke) { - this.SetBool("thru_smoke", thruSmoke); - } + public get() { + return this.GetBool("thru_smoke"); + } + public set(bool thruSmoke) { + this.SetBool("thru_smoke", thruSmoke); + } } property int Penetrated { - public get() { - return this.GetInt("penetrated"); - } - - public set(int penetrated) { - this.SetInt("penetrated", penetrated); - } + public get() { + return this.GetInt("penetrated"); + } + public set(int penetrated) { + this.SetInt("penetrated", penetrated); + } } property bool AttackerBlind { - public get() { - return this.GetBool("attacker_blind"); - } - - public set(bool blind) { - this.SetBool("attacker_blind", blind); - } + public get() { + return this.GetBool("attacker_blind"); + } + public set(bool blind) { + this.SetBool("attacker_blind", blind); + } } property bool NoScope { - public get() { - return this.GetBool("no_scope"); - } - - public set(bool noScope) { - this.SetBool("no_scope", noScope); - } + public get() { + return this.GetBool("no_scope"); + } + public set(bool noScope) { + this.SetBool("no_scope", noScope); + } } property bool Suicide { - public get() { - return this.GetBool("suicide"); - } - - public set(bool suicide) { - this.SetBool("suicide", suicide); - } + public get() { + return this.GetBool("suicide"); + } + public set(bool suicide) { + this.SetBool("suicide", suicide); + } } property bool FriendlyFire { - public get() { - return this.GetBool("friendly_fire"); - } - - public set(bool friendlyFire) { - this.SetBool("friendly_fire", friendlyFire); - } + public get() { + return this.GetBool("friendly_fire"); + } + public set(bool friendlyFire) { + this.SetBool("friendly_fire", friendlyFire); + } } property Get5Player Attacker { - public get() { - return view_as(this.GetObject("attacker")); - } - - public set(Get5Player attacker) { - this.SetObject("attacker", attacker); - } + public get() { + return view_as(this.GetObject("attacker")); + } + public set(Get5Player attacker) { + this.SetObject("attacker", attacker); + } } property Get5AssisterObject Assist { - public get() { - return view_as(this.GetObject("assist")); - } - - public set(Get5AssisterObject assister) { - this.SetObject("assist", assister); - } + public get() { + return view_as(this.GetObject("assist")); + } + public set(Get5AssisterObject assister) { + this.SetObject("assist", assister); + } } // Use before accessing "Assist", as its getter will raise an exception if null. @@ -1323,270 +1263,256 @@ methodmap Get5PlayerDeathEvent < Get5PlayerWeaponEvent { const bool attackerBlind, const bool suicide, const int penetrated, - const bool bomb - ) { - - Get5PlayerDeathEvent self = view_as(new JSON_Object()); - self.SetEvent("player_death"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.Player = victim; - self.Weapon = weapon; - self.Headshot = headshot; - self.FriendlyFire = friendlyFire; - self.ThruSmoke = thruSmoke; - self.NoScope = noScope; - self.AttackerBlind = attackerBlind; - self.Suicide = suicide; - self.Penetrated = penetrated; - self.Bomb = bomb; - - // set nullables to null initially - self.SetObject("assist", null); - self.SetObject("attacker", null); - return self; - - } + const bool bomb) { + Get5PlayerDeathEvent self = view_as(new JSON_Object()); + self.SetEvent("player_death"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.Player = victim; + self.Weapon = weapon; + self.Headshot = headshot; + self.FriendlyFire = friendlyFire; + self.ThruSmoke = thruSmoke; + self.NoScope = noScope; + self.AttackerBlind = attackerBlind; + self.Suicide = suicide; + self.Penetrated = penetrated; + self.Bomb = bomb; + + // set nullables to null initially + self.SetObject("assist", null); + self.SetObject("attacker", null); + return self; + } } // GRENADES methodmap Get5GrenadeThrownEvent < Get5PlayerWeaponEvent { - public Get5GrenadeThrownEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player, const Get5Weapon weapon) { - Get5GrenadeThrownEvent self = view_as(new JSON_Object()); - self.SetEvent("grenade_thrown"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.Player = player; - self.Weapon = weapon; - return self; - } + public Get5GrenadeThrownEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player, const Get5Weapon weapon) { + Get5GrenadeThrownEvent self = view_as(new JSON_Object()); + self.SetEvent("grenade_thrown"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.Player = player; + self.Weapon = weapon; + return self; + } } methodmap Get5VictimGrenadeEvent < Get5PlayerWeaponEvent { - // Array of either Get5DamageGrenadeVictim or Get5BlindedGrenadeVictim - property JSON_Array Victims { - public get() { - return view_as(this.GetObject("victims")); - } - - public set(JSON_Array victims) { - this.SetObject("victims", victims); - } + // Array of either Get5DamageGrenadeVictim or Get5BlindedGrenadeVictim + property JSON_Array Victims { + public get() { + return view_as(this.GetObject("victims")); + } + public set(JSON_Array victims) { + this.SetObject("victims", victims); } + } } methodmap Get5VictimWithDamageGrenadeEvent < Get5VictimGrenadeEvent { - property int DamageEnemies { - public get() { - return this.GetInt("damage_enemies"); - } - - public set(int damage) { - this.SetInt("damage_enemies", damage); - } + property int DamageEnemies { + public get() { + return this.GetInt("damage_enemies"); } + public set(int damage) { + this.SetInt("damage_enemies", damage); + } + } - property int DamageFriendlies { - public get() { - return this.GetInt("damage_friendlies"); - } - - public set(int damage) { - this.SetInt("damage_friendlies", damage); - } + property int DamageFriendlies { + public get() { + return this.GetInt("damage_friendlies"); } + public set(int damage) { + this.SetInt("damage_friendlies", damage); + } + } } methodmap Get5SmokeDetonatedEvent < Get5PlayerWeaponEvent { - property bool ExtinguishedMolotov { - public get() { - return this.GetBool("extinguished_molotov"); - } - - public set(bool extinguishedMolotov) { - this.SetBool("extinguished_molotov", extinguishedMolotov); - } + property bool ExtinguishedMolotov { + public get() { + return this.GetBool("extinguished_molotov"); } - - public Get5SmokeDetonatedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player, bool extinguishedMolotov) { - Get5SmokeDetonatedEvent self = view_as(new JSON_Object()); - self.SetEvent("smokegrenade_detonated"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.Player = player; - self.Weapon = new Get5Weapon("smokegrenade", CSWeapon_SMOKEGRENADE); - self.ExtinguishedMolotov = extinguishedMolotov; - return self; + public set(bool extinguishedMolotov) { + this.SetBool("extinguished_molotov", extinguishedMolotov); } + } + + public Get5SmokeDetonatedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player, bool extinguishedMolotov) { + Get5SmokeDetonatedEvent self = view_as(new JSON_Object()); + self.SetEvent("smokegrenade_detonated"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.Player = player; + self.Weapon = new Get5Weapon("smokegrenade", CSWeapon_SMOKEGRENADE); + self.ExtinguishedMolotov = extinguishedMolotov; + return self; + } } methodmap Get5HEDetonatedEvent < Get5VictimWithDamageGrenadeEvent { - public Get5HEDetonatedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player) { - Get5HEDetonatedEvent self = view_as(new JSON_Object()); - self.SetEvent("hegrenade_detonated"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.Player = player; - self.Weapon = new Get5Weapon("hegrenade", CSWeapon_HEGRENADE); - self.Victims = new JSON_Array(); - self.DamageEnemies = 0; - self.DamageFriendlies = 0; - return self; - } + public Get5HEDetonatedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player) { + Get5HEDetonatedEvent self = view_as(new JSON_Object()); + self.SetEvent("hegrenade_detonated"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.Player = player; + self.Weapon = new Get5Weapon("hegrenade", CSWeapon_HEGRENADE); + self.Victims = new JSON_Array(); + self.DamageEnemies = 0; + self.DamageFriendlies = 0; + return self; + } } methodmap Get5FlashbangDetonatedEvent < Get5VictimGrenadeEvent { - public Get5FlashbangDetonatedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player) { - Get5FlashbangDetonatedEvent self = view_as(new JSON_Object()); - self.SetEvent("flashbang_detonated"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.Player = player; - self.Weapon = new Get5Weapon("flashbang", CSWeapon_FLASHBANG); - self.Victims = new JSON_Array(); - return self; - } + public Get5FlashbangDetonatedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player) { + Get5FlashbangDetonatedEvent self = view_as(new JSON_Object()); + self.SetEvent("flashbang_detonated"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.Player = player; + self.Weapon = new Get5Weapon("flashbang", CSWeapon_FLASHBANG); + self.Victims = new JSON_Array(); + return self; + } } methodmap Get5DecoyStartedEvent < Get5PlayerWeaponEvent { - public Get5DecoyStartedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player) { - Get5DecoyStartedEvent self = view_as(new JSON_Object()); - self.SetEvent("decoygrenade_started"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.Player = player; - self.Weapon = new Get5Weapon("decoy", CSWeapon_DECOY); - return self; - } + public Get5DecoyStartedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player) { + Get5DecoyStartedEvent self = view_as(new JSON_Object()); + self.SetEvent("decoygrenade_started"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.Player = player; + self.Weapon = new Get5Weapon("decoy", CSWeapon_DECOY); + return self; + } } // This event fires when the molotov ends, but its RoundTime parameter is when it started burning. // Note that this event does *not* fire if the molotov was thrown directly at a smoke and did not start burning. methodmap Get5MolotovDetonatedEvent < Get5VictimWithDamageGrenadeEvent { - // RoundTime is when the molotov detonated. - property int EndTime { - public get() { - return this.GetInt("round_time_ended"); - } - - public set(int endTime) { - this.SetInt("round_time_ended", endTime); - this.SetInt("duration", endTime - this.RoundTime); - } + // RoundTime is when the molotov detonated. + property int EndTime { + public get() { + return this.GetInt("round_time_ended"); } - - public Get5MolotovDetonatedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player) { - Get5MolotovDetonatedEvent self = view_as(new JSON_Object()); - self.SetEvent("molotov_detonated"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.Player = player; - self.Weapon = new Get5Weapon("molotov", CSWeapon_MOLOTOV); // Sourcemod does not give us the info required to distinguish between molly and firebomb - self.Victims = new JSON_Array(); - self.EndTime = 0; // Set after the molotov stops burning (either by expiration, extinguish or new round start). - self.DamageEnemies = 0; - self.DamageFriendlies = 0; - return self; + public set(int endTime) { + this.SetInt("round_time_ended", endTime); + this.SetInt("duration", endTime - this.RoundTime); } + } + + public Get5MolotovDetonatedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player) { + Get5MolotovDetonatedEvent self = view_as(new JSON_Object()); + self.SetEvent("molotov_detonated"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.Player = player; + self.Weapon = new Get5Weapon("molotov", CSWeapon_MOLOTOV); // SourceMod does not give us the info required to distinguish between molly and firebomb + self.Victims = new JSON_Array(); + self.EndTime = 0; // Set after the molotov stops burning (either by expiration, extinguish or new round start). + self.DamageEnemies = 0; + self.DamageFriendlies = 0; + return self; + } } methodmap Get5GrenadeVictim < JSON_Object { - property Get5Player Player { - public get() { - return view_as(this.GetObject("player")); - } - - public set(Get5Player player) { - this.SetObject("player", player); - } + property Get5Player Player { + public get() { + return view_as(this.GetObject("player")); } - - property bool FriendlyFire { - public get() { - return this.GetBool("friendly_fire"); - } - - public set(bool friendlyFire) { - this.SetBool("friendly_fire", friendlyFire); - } + public set(Get5Player player) { + this.SetObject("player", player); } + } + property bool FriendlyFire { + public get() { + return this.GetBool("friendly_fire"); + } + public set(bool friendlyFire) { + this.SetBool("friendly_fire", friendlyFire); + } + } } methodmap Get5DamageGrenadeVictim < Get5GrenadeVictim { - property int Damage { - public get() { - return this.GetInt("damage"); - } - - public set(int damage) { - this.SetInt("damage", damage); - } + property int Damage { + public get() { + return this.GetInt("damage"); } - - property bool Killed { - public get() { - return this.GetBool("killed"); - } - - public set(bool killed) { - this.SetBool("killed", killed); - } + public set(int damage) { + this.SetInt("damage", damage); } + } - public Get5DamageGrenadeVictim(const Get5Player player, const bool friendlyFire, bool killed, const int damage) { - Get5DamageGrenadeVictim self = view_as(new JSON_Object()); - self.Player = player - self.FriendlyFire = friendlyFire; - self.Killed = killed; - self.Damage = damage; - return self; + property bool Killed { + public get() { + return this.GetBool("killed"); } + public set(bool killed) { + this.SetBool("killed", killed); + } + } + + public Get5DamageGrenadeVictim(const Get5Player player, const bool friendlyFire, bool killed, const int damage) { + Get5DamageGrenadeVictim self = view_as(new JSON_Object()); + self.Player = player + self.FriendlyFire = friendlyFire; + self.Killed = killed; + self.Damage = damage; + return self; + } } methodmap Get5BlindedGrenadeVictim < Get5GrenadeVictim { - property float BlindDuration { - public get() { - return this.GetFloat("blind_duration"); - } - - public set(float blindDuration) { - this.SetFloat("blind_duration", blindDuration); - } + property float BlindDuration { + public get() { + return this.GetFloat("blind_duration"); } - - public Get5BlindedGrenadeVictim(const Get5Player player, const bool friendlyFire, const float blindDuration) { - Get5BlindedGrenadeVictim self = view_as(new JSON_Object()); - self.Player = player - self.FriendlyFire = friendlyFire; - self.BlindDuration = blindDuration; - return self; + public set(float blindDuration) { + this.SetFloat("blind_duration", blindDuration); } + } + + public Get5BlindedGrenadeVictim(const Get5Player player, const bool friendlyFire, const float blindDuration) { + Get5BlindedGrenadeVictim self = view_as(new JSON_Object()); + self.Player = player + self.FriendlyFire = friendlyFire; + self.BlindDuration = blindDuration; + return self; + } } // BOMB @@ -1607,71 +1533,151 @@ methodmap Get5BombEvent < Get5TimedRoundEvent { methodmap Get5PlayerBombEvent < Get5PlayerTimedRoundEvent { - property Get5BombSite Site { - public get() { - return view_as(this.GetInt("site_int")); - } - public set(Get5BombSite site) { - this.SetInt("site_int", view_as(site)); - this.SetHidden("site_int", true); - ConvertBombSiteToStringInJson(this, "site", site); - } + property Get5BombSite Site { + public get() { + return view_as(this.GetInt("site_int")); } + public set(Get5BombSite site) { + this.SetInt("site_int", view_as(site)); + this.SetHidden("site_int", true); + ConvertBombSiteToStringInJson(this, "site", site); + } + } } methodmap Get5BombPlantedEvent < Get5PlayerBombEvent { - public Get5BombPlantedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player, const Get5BombSite site) { - Get5BombPlantedEvent self = view_as(new JSON_Object()); - self.SetEvent("bomb_planted"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.Player = player; - self.Site = site; - return self; - } + public Get5BombPlantedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5Player player, const Get5BombSite site) { + Get5BombPlantedEvent self = view_as(new JSON_Object()); + self.SetEvent("bomb_planted"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.Player = player; + self.Site = site; + return self; + } } methodmap Get5BombExplodedEvent < Get5BombEvent { - public Get5BombExplodedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5BombSite site) { - Get5BombExplodedEvent self = view_as(new JSON_Object()); - self.SetEvent("bomb_exploded"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.Site = site; - return self; - } + public Get5BombExplodedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, const Get5BombSite site) { + Get5BombExplodedEvent self = view_as(new JSON_Object()); + self.SetEvent("bomb_exploded"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.Site = site; + return self; + } } methodmap Get5BombDefusedEvent < Get5PlayerBombEvent { - property int TimeRemaining { - public get() { - return this.GetInt("bomb_time_remaining"); - } - public set(int time) { - this.SetInt("bomb_time_remaining", time); - } + property int TimeRemaining { + public get() { + return this.GetInt("bomb_time_remaining"); } - - public Get5BombDefusedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, - const Get5Player player, const Get5BombSite site, const int timeRemaining) { - Get5BombDefusedEvent self = view_as(new JSON_Object()); - self.SetEvent("bomb_defused"); - self.SetMatchId(matchId); - self.MapNumber = mapNumber; - self.RoundNumber = roundNumber; - self.RoundTime = roundTime; - self.Player = player; - self.Site = site; - self.TimeRemaining = timeRemaining; - return self; + public set(int time) { + this.SetInt("bomb_time_remaining", time); } + } + + public Get5BombDefusedEvent(const char[] matchId, const int mapNumber, const int roundNumber, const int roundTime, + const Get5Player player, const Get5BombSite site, const int timeRemaining) { + Get5BombDefusedEvent self = view_as(new JSON_Object()); + self.SetEvent("bomb_defused"); + self.SetMatchId(matchId); + self.MapNumber = mapNumber; + self.RoundNumber = roundNumber; + self.RoundTime = roundTime; + self.Player = player; + self.Site = site; + self.TimeRemaining = timeRemaining; + return self; + } +} + +stock void GameStateString(const Get5State state, char[] buffer, const int length) { + switch (state) { + case Get5State_None: + Format(buffer, length, "none"); + case Get5State_PreVeto: + Format(buffer, length, "pre_veto"); + case Get5State_Veto: + Format(buffer, length, "veto"); + case Get5State_Warmup: + Format(buffer, length, "warmup"); + case Get5State_KnifeRound: + Format(buffer, length, "knife"); + case Get5State_WaitingForKnifeRoundDecision: + Format(buffer, length, "waiting_for_knife_decision"); + case Get5State_GoingLive: + Format(buffer, length, "going_live"); + case Get5State_Live: + Format(buffer, length, "live"); + case Get5State_PostGame: + Format(buffer, length, "post_game"); + } +} + +stock void ConvertGameStateToStringInJson(const JSON_Object obj, const char[] key, + const Get5State state) { + char gameStateString[64]; + GameStateString(state, gameStateString, sizeof(gameStateString)); + obj.SetString(key, gameStateString); +} + +stock void ConvertGet5SideToStringInJson(const JSON_Object obj, const char[] key, Get5Side side) { + if (side == Get5Side_T) { + obj.SetString(key, "t"); + } else if (side == Get5Side_CT) { + obj.SetString(key, "ct"); + } else if (side == Get5Side_Spec) { + obj.SetString(key, "spec"); + } else { + obj.SetObject(key, null); + } +} + +stock void ConvertGet5TeamToStringInJson(const JSON_Object obj, const char[] key, Get5Team team) { + if (team == Get5Team_1) { + obj.SetString(key, "team1"); + } else if (team == Get5Team_2) { + obj.SetString(key, "team2"); + } else if (team == Get5Team_Spec) { + obj.SetString(key, "spec"); + } else { + obj.SetObject(key, null); + } +} + +stock void ConvertGet5PauseTypeToStringInJson(const JSON_Object obj, const char[] key, + Get5PauseType pauseType) { + if (pauseType == Get5PauseType_Admin) { + obj.SetString(key, "admin"); + } else if (pauseType == Get5PauseType_Tech) { + obj.SetString(key, "technical"); + } else if (pauseType == Get5PauseType_Tactical) { + obj.SetString(key, "tactical"); + } else if (pauseType == Get5PauseType_Backup) { + obj.SetString(key, "backup"); + } else { + obj.SetObject(key, null); + } +} + +stock void ConvertBombSiteToStringInJson(const JSON_Object obj, const char[] key, + const Get5BombSite site) { + if (site == Get5BombSite_A) { + obj.SetString(key, "a"); + } else if (site == Get5BombSite_B) { + obj.SetString(key, "b"); + } else { + obj.SetObject(key, null); + } } // Called each get5-event with JSON formatted event text. @@ -1779,7 +1785,8 @@ forward void Get5_OnMatchPaused(const Get5MatchPausedEvent event); forward void Get5_OnMatchUnpaused(const Get5MatchUnpausedEvent event); // Called when a match backup is restored. -// Note that the match ID and map number is the one being restored *to*, not the current game state and the time the backup is loaded. +// Note that the match ID, map number and round number is the one being restored *to*, not the current game state at the +// time the backup is loaded. forward void Get5_OnBackupRestore(const Get5BackupRestoredEvent event); // Series stats (root section) @@ -1798,6 +1805,9 @@ forward void Get5_OnBackupRestore(const Get5BackupRestoredEvent event); #define STAT_TEAMSCORE "score" // Player stats (under map section, then team section, then player's steam64) +// If adding stuff here, also add to the InitPlayerStats function! +#define STAT_INIT "init" // used to zero-fill stats only. Not a real stat. +#define STAT_COACHING "coaching" // indicates if the player is a coach. #define STAT_NAME "name" #define STAT_KILLS "kills" #define STAT_DEATHS "deaths" @@ -1864,6 +1874,6 @@ public __pl_get5_SetNTVOptional() { MarkNativeAsOptional("Get5_AddLiveCvar"); MarkNativeAsOptional("Get5_IncreasePlayerStat"); MarkNativeAsOptional("Get5_GetMatchStats"); - MarkNativeAsOptinoal("Get5_GetMapNumber"); + MarkNativeAsOptional("Get5_GetMapNumber"); } #endif diff --git a/scripting/include/restorecvars.inc b/scripting/include/restorecvars.inc index ae3e16e5c..d197e2024 100644 --- a/scripting/include/restorecvars.inc +++ b/scripting/include/restorecvars.inc @@ -27,6 +27,10 @@ stock Handle SaveCvars(ArrayList cvarNames) { // Restores cvars to their previous value using a return value of SaveCvars. stock void RestoreCvars(Handle& cvarStorage, bool close = true) { + LogDebug("Restoring match cvars."); + if (cvarStorage == INVALID_HANDLE) { + return; + } ArrayList cvarNameList = view_as(GetArrayCell(cvarStorage, 0)); ArrayList cvarValueList = view_as(GetArrayCell(cvarStorage, 1)); diff --git a/translations/chi/get5.phrases.txt b/translations/chi/get5.phrases.txt index 98e1d73cf..b7e055548 100644 --- a/translations/chi/get5.phrases.txt +++ b/translations/chi/get5.phrases.txt @@ -2,23 +2,23 @@ { "ReadyToVetoInfoMessage" { - "chi" "当您的队伍准备好进行Veto(Ban图)时,请输入{GREEN}!ready{NORMAL}。" + "chi" "当您的队伍准备好进行Veto(Ban图)时,请输入{1}。" } "WaitingForCastersReadyInfoMessage" { - "chi" "正在等待裁判输入{GREEN}!ready{NORMAL}来开始比赛。" + "chi" "正在等待裁判输入{2}来开始比赛。" } "ReadyToRestoreBackupInfoMessage" { - "chi" "当您准备好恢复比赛至备份档案时,请输入{GREEN}!ready{NORMAL}。" + "chi" "当您准备好恢复比赛至备份档案时,请输入{1}。" } "ReadyToKnifeInfoMessage" { - "chi" "当您准备好进行刀局时,请输入{GREEN}!ready{NORMAL}。" + "chi" "当您准备好进行刀局时,请输入{1}。" } "ReadyToStartInfoMessage" { - "chi" "当您准备好开始比赛时,请输入{GREEN}!ready{NORMAL}。" + "chi" "当您准备好开始比赛时,请输入{1}。" } "YouAreReady" { @@ -30,7 +30,7 @@ } "WaitingForEnemySwapInfoMessage" { - "chi" "{1}赢得刀局。等待他们输入!stay或是!swapwon来选择留下还是交换队伍。" + "chi" "{1}赢得刀局。等待他们输入{2}或是{3}来选择留下还是交换队伍。" } "WaitingForGOTVBrodcastEndingInfoMessage" { @@ -86,11 +86,11 @@ } "MatchUnpauseInfoMessage" { - "chi" "{1:N}解除了比赛的暂停。" + "chi" "{1}解除了比赛的暂停。" } "WaitingForUnpauseInfoMessage" { - "chi" "{1}想要取消暂停,正在等待{2}输入!unpause来取消暂停。" + "chi" "{1}想要取消暂停,正在等待{2}输入{3}来取消暂停。" } "PausesLeftInfoMessage" { @@ -122,11 +122,11 @@ } "ForceReadyInfoMessage" { - "chi" "如果您有不到{1}名玩家,您可以输入{GREEN}!forceready{NORMAL}来强制准备您的队伍。" + "chi" "如果您有不到{1}名玩家,您可以输入{2}来强制准备您的队伍。" } "TeammateForceReadied" { - "chi" "您的队伍已被{GREEN}{1}{NORMAL}强制准备就绪。" + "chi" "您的队伍已被{1}强制准备就绪。" } "AdminForceReadyInfoMessage" { @@ -144,13 +144,13 @@ { "chi" "一名管理员强制解除了比赛暂停。" } - "TeamWantsToReloadLastRoundInfoMessage" + "TeamWantsToReloadCurrentRound" { - "chi" "{1}想停止比赛并重新加载比赛至上一回合,需要{2}输入!stop来确认。" + "chi" "{1}想停止比赛并重新加载比赛至上一回合,需要{2}输入{3}来确认。" } "TeamWinningSeriesInfoMessage" { - "chi" "{1}{NORMAL}在这系列比赛中正处于领先状态 {2}-{3}" + "chi" "{1}在这系列比赛中正处于领先状态 {2}-{3}" } "SeriesTiedInfoMessage" { @@ -158,7 +158,7 @@ } "NextSeriesMapInfoMessage" { - "chi" "下一张地图在此系列比赛中是{GREEN}{1}" + "chi" "下一张地图在此系列比赛中是{1}" } "TeamWonMatchInfoMessage" { @@ -168,22 +168,14 @@ { "chi" "{1}与{2}打成了平局。" } - "TeamsSplitSeriesBO2InfoMessage" - { - "chi" "{1}和{2}在此系列比赛中平局 1-1。" - } "TeamWonSeriesInfoMessage" { - "chi" "{1}赢得了此系列比赛 {2}-{3}." + "chi" "{1}赢得了此系列比赛 {2}-{3}" } "MatchFinishedInfoMessage" { "chi" "这场比赛已完结。" } - "CurrentScoreInfoMessage" - { - "chi" "{LIGHT_GREEN}{1} {GREEN}{2} {NORMAL}- {GREEN}{3} {LIGHT_GREEN}{4}" - } "BackupLoadedInfoMessage" { "chi" "成功加载备份档案 {1}" @@ -218,7 +210,7 @@ } "ChangingMapInfoMessage" { - "chi" "切换地图至{GREEN}{1}......" + "chi" "切换地图至{1}..." } "MapDecidedInfoMessage" { @@ -226,19 +218,19 @@ } "MapIsInfoMessage" { - "chi" "地图 {1}: {GREEN}{2}" + "chi" "地图 {1}: {2}" } "TeamPickedMapInfoMessage" { - "chi" "{1}挑选了{GREEN}{2}{NORMAL}作为地图 {3}" + "chi" "{1}挑选了{2}作为地图 {3}" } "TeamSelectSideInfoMessage" { - "chi" "{1}已选择在{GREEN}{2}{NORMAL}开始 {3}" + "chi" "{1}已选择在{2}开始 {3}" } "TeamVetoedMapInfoMessage" { - "chi" "{1}vetoed(Ban掉)了{LIGHT_RED}{2}" + "chi" "{1}vetoed(Ban掉)了{2}" } "CaptainLeftOnVetoInfoMessage" { @@ -246,7 +238,7 @@ } "ReadyToResumeVetoInfoMessage" { - "chi" "当您准备好恢复Veto(Ban图)时,请输入{GREEN}!ready{NORMAL}。" + "chi" "当您准备好恢复Veto(Ban图)时,请输入{1}。" } "MatchConfigLoadedInfoMessage" { @@ -264,10 +256,6 @@ { "chi" "[未准备]" } - "MatchPoweredBy" - { - "chi" "由{YELLOW}Get5{NORMAL}强力驱动" - } "MapVetoPickMenuText" { "chi" "请选择一张想玩的地图:" @@ -302,6 +290,6 @@ } "VetoCountdown" { - "chi" "Veto(Ban图)还剩{GREEN}{1}秒。" + "chi" "Veto(Ban图)还剩{1}秒。" } } diff --git a/translations/da/get5.phrases.txt b/translations/da/get5.phrases.txt index 2978c6fa5..7ee6e30e8 100644 --- a/translations/da/get5.phrases.txt +++ b/translations/da/get5.phrases.txt @@ -2,23 +2,23 @@ { "ReadyToVetoInfoMessage" { - "da" "Skriv {GREEN}!ready{NORMAL} når dit hold er klar til at veto." + "da" "Skriv {1} når dit hold er klar til at veto." } "WaitingForCastersReadyInfoMessage" { - "da" "Venter på, at {1} skriver {GREEN}!ready {NORMAL}." + "da" "Venter på, at {1} skriver {2}." } "ReadyToRestoreBackupInfoMessage" { - "da" "Skriv {GREEN}!ready {NORMAL}når du er klar til at genoprette match backup." + "da" "Skriv {1} når du er klar til at genoprette match backup." } "ReadyToKnifeInfoMessage" { - "da" "Skriv {GREEN}!ready {NORMAL}når du er klar til at knife." + "da" "Skriv {1} når du er klar til at knife." } "ReadyToStartInfoMessage" { - "da" "Skriv {GREEN}!ready {NORMAL}når du er klar til at spille." + "da" "Skriv {1} når du er klar til at spille." } "YouAreReady" { @@ -26,7 +26,7 @@ } "YouAreReadyAuto" { - "da" "NOTE: Du er blevet markeret som klar grundet din spilaktivitet. Skriv !unready hvis du ikke er klar." + "da" "NOTE: Du er blevet markeret som klar grundet spilaktivitet. Skriv {1} hvis du ikke er klar." } "YouAreNotReady" { @@ -34,7 +34,7 @@ } "WaitingForEnemySwapInfoMessage" { - "da" "{1} vandt knife-runden. Venter på, at de skriver !stay eller !swap." + "da" "{1} vandt knife-runden. Venter på, at de skriver {2} eller {3}." } "WaitingForGOTVBrodcastEndingInfoMessage" { @@ -138,7 +138,7 @@ } "TechPauseRunoutInfoMessage" { - "da" "Maksimal teknisk pauselængde er nået, og alle kan nu !unpause." + "da" "Maksimal teknisk pauselængde er nået, og alle kan nu {1}." } "TechPauseNoTimeRemaining" { @@ -158,7 +158,7 @@ } "WaitingForUnpauseInfoMessage" { - "da" "{1} vil gerne fortsætte kampen. Afventer !unpause fra {2}." + "da" "{1} vil gerne fortsætte kampen. Afventer {3} fra {2}." } "PausesLeftInfoMessage" { @@ -190,11 +190,11 @@ } "ForceReadyInfoMessage" { - "da" "Du kan skrive {GREEN}!forceready {NORMAL} for tvinge dit hold klar, hvis der er mindre end {GREEN}{1}{NORMAL} spillere." + "da" "Du kan skrive {1} for tvinge dit hold klar, hvis der er mindre end {2} spillere." } "TeammateForceReadied" { - "da" "Dit hold blev tvunget klar af {GREEN}{1}." + "da" "Dit hold blev tvunget klar af {1}." } "AdminForceReadyInfoMessage" { @@ -204,6 +204,10 @@ { "da" "En admin har tvunget kampen til at slutte." } + "AdminForceEndWithWinnerInfoMessage" + { + "da" "En admin har tvunget kampen til at slutte med {1} som vinder." + } "AdminForcePauseInfoMessage" { "da" "En admin har tvunget kampen til at pause." @@ -212,21 +216,21 @@ { "da" "En admin har tvunget kampen til at fortsætte." } - "TeamWantsToReloadLastRoundInfoMessage" + "TeamWantsToReloadCurrentRound" { - "da" "{1} vil stoppe og genindlæse sidste runde. Afventer !stop fra {2}." + "da" "{1} vil stoppe og genindlæse til starten af denne runde. Afventer {3} fra {2}." } "TeamWinningSeriesInfoMessage" { - "da" "{1}{NORMAL} vinder serien {2}-{3}" + "da" "{1} fører serien {2}-{3}." } "SeriesTiedInfoMessage" { - "da" "Serien er uafgjort {1}-{2}" + "da" "Serien er uafgjort {1}-{2}." } "NextSeriesMapInfoMessage" { - "da" "Det næste map i serien er{GREEN}{1}" + "da" "Det næste map i serien er {1} og det starter om {2}." } "TeamWonMatchInfoMessage" { @@ -234,11 +238,7 @@ } "TeamTiedMatchInfoMessage" { - "da" "{1} og {2} har spillet kampen uafgjort." - } - "TeamsSplitSeriesBO2InfoMessage" - { - "da" "{1} og {2} har splittet serien 1-1." + "da" "{1} og {2} har spillet uafgjort." } "TeamWonSeriesInfoMessage" { @@ -248,14 +248,14 @@ { "da" "Kampen er afsluttet" } - "CurrentScoreInfoMessage" - { - "da" "{LIGHT_GREEN}{1} {GREEN}{2} {NORMAL}- {GREEN}{3} {LIGHT_GREEN}{4}" - } "StopCommandNotEnabled" { "da" "Stop-kommandoen er ikke aktiveret." } + "StopCommandVotingReset" + { + "da" "Anmodningen fra {1} om at stoppe spillet blev annulleret, da runden sluttede." + } "BackupLoadedInfoMessage" { "da" "Backup {1} indlæst." @@ -290,7 +290,7 @@ } "ChangingMapInfoMessage" { - "da" "Skifter map til {GREEN}{1}..." + "da" "Skifter map til {1}..." } "MapDecidedInfoMessage" { @@ -298,19 +298,19 @@ } "MapIsInfoMessage" { - "da" "Map {1}: {GREEN}{2}" + "da" "Map {1}: {2}" } "TeamPickedMapInfoMessage" { - "da" "{1} valgte {GREEN}{2} {NORMAL}som deres map {3}" + "da" "{1} valgte {2} som deres {3}. map." } "TeamSelectSideInfoMessage" { - "da" "{1} har valgt at start som {GREEN}{2} {NORMAL}på {3}" + "da" "{1} har valgt at start som {2} på {3}." } "TeamVetoedMapInfoMessage" { - "da" "{1} vetoed {LIGHT_RED}{2}" + "da" "{1} vetoed {2}." } "CaptainLeftOnVetoInfoMessage" { @@ -318,7 +318,7 @@ } "ReadyToResumeVetoInfoMessage" { - "da" "Skriv {GREEN}!ready {NORMAL}når du er klar til at genoptage veto." + "da" "Skriv {1}, når du er klar til at genoptage veto." } "MatchConfigLoadedInfoMessage" { @@ -326,7 +326,27 @@ } "MoveToCoachInfoMessage" { - "da" "Da dit hold er fuldt, blev du flyttet til trænerposition." + "da" "Da dit hold er fyldt, blev du flyttet til trænerposition." + } + "CannotLeaveCoachingTeamIsFull" + { + "da" "Du kan ikke forlade trænerpositionen, da dit hold er fyldt." + } + "CoachingNotEnabled" + { + "da" "Trænerindstillingen er ikke aktiveret. {1} skal sættes til 1." + } + "PlayerIsCoachingTeam" + { + "da" "{1} er træner for {2}." + } + "CanOnlyCoachDuringWarmup" + { + "da" "Du kan kun skifte til og fra trænerposition under opvarmning," + } + "AllCoachSlotsFilledForTeam" + { + "da" "Alle trænerpositioner ({1}) for dit hold er fyldt." } "ReadyTag" { @@ -336,10 +356,6 @@ { "da" "[IKKE KLAR]" } - "MatchPoweredBy" - { - "da" "Drevet af {YELLOW}Get5" - } "MapVetoPickMenuText" { "da" "Vælg et map at spille:" @@ -374,6 +390,14 @@ } "VetoCountdown" { - "da" "Veto begynder om {GREEN}{1} {NORMAL}sekunder." + "da" "Veto begynder om {1} sekunder." + } + "NewVersionAvailable" + { + "da" "En nyere version af Get5 er tilgængelig. Opdatering kan findes på {1}." + } + "PrereleaseVersionWarning" + { + "da" "Serveren kører en uofficiel version af Get5 ({1}) designet til test og udvikling. Denne besked kan fjernes via {2}." } } diff --git a/translations/de/get5.phrases.txt b/translations/de/get5.phrases.txt index 4937241e0..9e9a85aa5 100644 --- a/translations/de/get5.phrases.txt +++ b/translations/de/get5.phrases.txt @@ -2,27 +2,27 @@ { "ReadyToVetoInfoMessage" { - "de" "Tippe {GREEN}!ready {NORMAL}wenn dein Team bereit zum Voten ist." + "de" "Tippe {1} wenn dein Team bereit zum Voten ist." } "WaitingForCastersReadyInfoMessage" { - "de" "Warten auf die Caster, tippe {GREEN}!ready {NORMAL}um zu beginnen." + "de" "Warten auf die Caster, tippe {2} um zu beginnen." } "ReadyToRestoreBackupInfoMessage" { - "de" "Tippe {GREEN}!ready {NORMAL}wenn dein Team bereit ist das Match Backup wieder einzuspielen." + "de" "Tippe {1} wenn dein Team bereit ist das Match Backup wieder einzuspielen." } "ReadyToKnifeInfoMessage" { - "de" "Tippe {GREEN}!ready {NORMAL}wenn dein Team bereit ist für die Messer-Runde." + "de" "Tippe {1} wenn dein Team bereit ist für die Messer-Runde." } "ReadyToStartInfoMessage" { - "de" "Tippe {GREEN}!ready {NORMAL}wenn dein Team bereit ist zu beginnen." + "de" "Tippe {1} wenn dein Team bereit ist zu beginnen." } "WaitingForEnemySwapInfoMessage" { - "de" "{1} gewinnt die Seitenwahl. Wartend auf !stay oder !swap ." + "de" "{1} gewinnt die Seitenwahl. Wartend auf {2} oder {3}." } "WaitingForGOTVBrodcastEndingInfoMessage" { @@ -74,11 +74,11 @@ } "MatchUnpauseInfoMessage" { - "de" "{1:N} hat das Match fortgesetzt." + "de" "{1} hat das Match fortgesetzt." } "WaitingForUnpauseInfoMessage" { - "de" "{1} möchte fortfahren, zum zuzustimmen muss {2} !unpause schreiben." + "de" "{1} möchte fortfahren, zum zuzustimmen muss {2} {3} schreiben." } "TeamFailToReadyMinPlayerCheck" { @@ -120,30 +120,26 @@ { "de" "Ein Admin hat das Match fortgesetzt." } - "TeamWantsToReloadLastRoundInfoMessage" + "TeamWantsToReloadCurrentRound" { - "de" "{1} möchte die letzte Runde wiederholen, {2} muss mit !stop bestätigen." + "de" "{1} möchte die letzte Runde wiederholen, {2} muss mit {3} bestätigen." } "TeamWinningSeriesInfoMessage" { - "de" "{1}{NORMAL} gewinnt die Serie {2}-{3}" + "de" "{1} gewinnt die Serie {2}-{3}." } "SeriesTiedInfoMessage" { - "de" "Die Serie ist unentschieden mit {1}-{2}" + "de" "Die Serie ist unentschieden mit {1}-{2}." } "NextSeriesMapInfoMessage" { - "de" "Die nächste Map in der Serie ist {GREEN}{1}" + "de" "Die nächste Map in der Serie ist {1}." } "TeamWonMatchInfoMessage" { "de" "{1} hat das Match gewonnen." } - "TeamsSplitSeriesBO2InfoMessage" - { - "de" "{1} und {2} haben die Serie 1-1 geteilt." - } "TeamWonSeriesInfoMessage" { "de" "{1} hat die Serie gewonnen {2}-{3}." @@ -152,13 +148,9 @@ { "de" "Das Match ist beendet" } - "CurrentScoreInfoMessage" - { - "de" "{LIGHT_GREEN}{1} {GREEN}{2} {NORMAL}- {GREEN}{3} {LIGHT_GREEN}{4}" - } "BackupLoadedInfoMessage" { - "de" "Backup {1} erfolgreich geladen" + "de" "Backup {1} erfolgreich geladen." } "MatchBeginInSecondsInfoMessage" { @@ -190,7 +182,7 @@ } "ChangingMapInfoMessage" { - "de" "Ändern der Map in {GREEN}{1}..." + "de" "Ändern der Map in {1}..." } "MapDecidedInfoMessage" { @@ -198,19 +190,19 @@ } "MapIsInfoMessage" { - "de" "Map {1}: {GREEN}{2}" + "de" "Map {1}: {2}" } "TeamPickedMapInfoMessage" { - "de" "{1} wählte {GREEN}{2} {NORMAL}als Map aus {3}" + "de" "{1} wählte {2} als Map aus {3}." } "TeamSelectSideInfoMessage" { - "de" "{1} möchte als {GREEN}{2} {NORMAL}starten {3}" + "de" "{1} möchte als {2} starten {3}." } "TeamVetoedMapInfoMessage" { - "de" "{1} verbietet {LIGHT_RED}{2}" + "de" "{1} verbietet {2}." } "CaptainLeftOnVetoInfoMessage" { @@ -218,7 +210,7 @@ } "ReadyToResumeVetoInfoMessage" { - "de" "Tippe {GREEN}!ready {NORMAL}wenn ihr fertig seit mit dem veto fortzufahren." + "de" "Tippe {1} wenn ihr fertig seit mit dem veto fortzufahren." } "MatchConfigLoadedInfoMessage" { diff --git a/translations/es/get5.phrases.txt b/translations/es/get5.phrases.txt index f502ac12c..c4fa6dc46 100644 --- a/translations/es/get5.phrases.txt +++ b/translations/es/get5.phrases.txt @@ -2,23 +2,23 @@ { "ReadyToVetoInfoMessage" { - "es" "Teclee {GREEN}!ready {NORMAL}cuando su equipo este listo para la ronda veto." + "es" "Teclee {1} cuando su equipo este listo para la ronda veto." } "WaitingForCastersReadyInfoMessage" { - "es" "Esperando : los casters deben teclear {GREEN}!ready {NORMAL}para que la partida comience." + "es" "Esperando : los casters deben teclear {2} para que la partida comience." } "ReadyToRestoreBackupInfoMessage" { - "es" "Teclee {GREEN}!ready {NORMAL}cuando este listo para restaurar el backup de la partida." + "es" "Teclee {1} cuando este listo para restaurar el backup de la partida." } "ReadyToKnifeInfoMessage" { - "es" "Teclee {GREEN}!ready {NORMAL}cuando este listo para empezar la ronda Knife." + "es" "Teclee {1} cuando este listo para empezar la ronda Knife." } "ReadyToStartInfoMessage" { - "es" "Teclee {GREEN}!ready {NORMAL}cuando este listo para empezar la ronda." + "es" "Teclee {1} cuando este listo para empezar la ronda." } "YouAreReady" { @@ -30,7 +30,7 @@ } "WaitingForEnemySwapInfoMessage" { - "es" "{1} ganó la ronda Knife. Esperando la elección entre !stay o !swap." + "es" "{1} ganó la ronda Knife. Esperando la elección entre {2} o {3}." } "WaitingForGOTVBrodcastEndingInfoMessage" { @@ -90,11 +90,11 @@ } "MatchUnpauseInfoMessage" { - "es" "{1:N} terminó su pausa." + "es" "{1} terminó su pausa." } "WaitingForUnpauseInfoMessage" { - "es" "{1} quiere retomar la partida, esperando a que {2} teclee !unpause" + "es" "{1} quiere retomar la partida, esperando a que {2} teclee {3}." } "PausesLeftInfoMessage" { @@ -126,11 +126,11 @@ } "ForceReadyInfoMessage" { - "es" "Puede teclear {GREEN}!forceready {NORMAL}para forzar su equipo a estar listo si tienen {GREEN}{1}{NORMAL} jugadores" + "es" "Puede teclear {1} para forzar su equipo a estar listo si tienen {2} jugadores" } "TeammateForceReadied" { - "es" "Su equipo a sido forzado a estar listo por {GREEN}{1}" + "es" "Su equipo a sido forzado a estar listo por {1}." } "AdminForceReadyInfoMessage" { @@ -148,21 +148,21 @@ { "es" "Un administrador forzó la recuperación de la partida." } - "TeamWantsToReloadLastRoundInfoMessage" + "TeamWantsToReloadCurrentRound" { - "es" "{1} quiere parar y reiniciar la partida, esto requiere {2} confirmaciones con !stop." + "es" "{1} quiere parar y reiniciar la partida, esto requiere {2} confirmaciones con {3}." } "TeamWinningSeriesInfoMessage" { - "es" "{1}{NORMAL} ganó las series {2}-{3}" + "es" "{1} ganó las series {2}-{3}." } "SeriesTiedInfoMessage" { - "es" "La serie empató {1}-{2}" + "es" "La serie empató {1}-{2}." } "NextSeriesMapInfoMessage" { - "es" "El proximo mapa de la serie es {GREEN}{1}" + "es" "El proximo mapa de la serie es {1}." } "TeamWonMatchInfoMessage" { @@ -172,10 +172,6 @@ { "es" "{1} y {2} empataron." } - "TeamsSplitSeriesBO2InfoMessage" - { - "es" "{1} y {2} han ganado la misma cantidad de partidas (1-1)." - } "TeamWonSeriesInfoMessage" { "es" "{1} ganó la serie {2}-{3}." @@ -184,13 +180,9 @@ { "es" "La partida está terminada" } - "CurrentScoreInfoMessage" - { - "es" "{LIGHT_GREEN}{1} {GREEN}{2} {NORMAL}- {GREEN}{3} {LIGHT_GREEN}{4}" - } "BackupLoadedInfoMessage" { - "es" "Backup cargado exitosamente {1}" + "es" "Backup cargado exitosamente {1}." } "MatchBeginInSecondsInfoMessage" { @@ -210,7 +202,7 @@ } "TeamDecidedToStayInfoMessage" { - "es" "{1} {1} decidiío quedarse.." + "es" "{1} {1} decidiío quedarse." } "TeamDecidedToSwapInfoMessage" { @@ -222,7 +214,7 @@ } "ChangingMapInfoMessage" { - "es" "Cambio de mapa por {GREEN}{1}…" + "es" "Cambio de mapa por {1}..." } "MapDecidedInfoMessage" { @@ -230,19 +222,19 @@ } "MapIsInfoMessage" { - "es" "Map {1}: {GREEN}{2}" + "es" "Map {1}: {2}" } "TeamPickedMapInfoMessage" { - "es" "{1} eligió {GREEN}{2} {NORMAL}de mapa {3}}" + "es" "{1} eligió {2} de mapa {3}." } "TeamSelectSideInfoMessage" { - "es" "{1} eligió empezar con {GREEN}{2} {NORMAL}sobre {3}" + "es" "{1} eligió empezar con {2} sobre {3}." } "TeamVetoedMapInfoMessage" { - "es" "{1} rechazó {LIGHT_RED}{2}" + "es" "{1} rechazó {2}." } "CaptainLeftOnVetoInfoMessage" { @@ -250,7 +242,7 @@ } "ReadyToResumeVetoInfoMessage" { - "es" "Teclee {GREEN}!ready {NORMAL}cuando este listo para retomar la partida de veto." + "es" "Teclee {1} cuando este listo para retomar la partida de veto." } "MatchConfigLoadedInfoMessage" { @@ -268,10 +260,6 @@ { "es" "[NO LISTO]" } - "MatchPoweredBy" - { - "es" "Gestionado con {YELLOW}Get5" - } "MapVetoPickMenuText" { "es" "Seleccionar una mapa para el juego:" @@ -306,6 +294,6 @@ } "VetoCountdown" { - "es" "El veto comenzará en {GREEN}{1} {NORMAL}segundos." + "es" "El veto comenzará en {1} segundos." } } diff --git a/translations/fr/get5.phrases.txt b/translations/fr/get5.phrases.txt index 793d623ad..e5e838f30 100644 --- a/translations/fr/get5.phrases.txt +++ b/translations/fr/get5.phrases.txt @@ -2,23 +2,23 @@ { "ReadyToVetoInfoMessage" { - "fr" "Entrez {GREEN}!ready {NORMAL}quand votre équipe est prête pour le tour de veto." + "fr" "Entrez {1} quand votre équipe est prête pour le tour de veto." } "WaitingForCastersReadyInfoMessage" { - "fr" "En attente : les casters doivent entrer {GREEN}!ready {NORMAL}pour que le match commence." + "fr" "En attente : les casters doivent entrer {2} pour que le match commence." } "ReadyToRestoreBackupInfoMessage" { - "fr" "Entrez {GREEN}!ready {NORMAL}quand vous êtes prêt(e) à restorer la sauvegarde du match." + "fr" "Entrez {1} quand vous êtes prêt(e) à restorer la sauvegarde du match." } "ReadyToKnifeInfoMessage" { - "fr" "Entrez {GREEN}!ready {NORMAL}quand vous êtes prêt(e) à démarrer un round au couteau." + "fr" "Entrez {1} quand vous êtes prêt(e) à démarrer un round au couteau." } "ReadyToStartInfoMessage" { - "fr" "Entrez {GREEN}!ready {NORMAL}quand vous êtes prêt(e) à commencer." + "fr" "Entrez {1} quand vous êtes prêt(e) à commencer." } "YouAreReady" { @@ -30,7 +30,7 @@ } "WaitingForEnemySwapInfoMessage" { - "fr" "{1} a gagné le round au couteau. En attente de leur choix entre !stay et !swap." + "fr" "{1} a gagné le round au couteau. En attente de leur choix entre {2} et {3}." } "WaitingForGOTVBrodcastEndingInfoMessage" { @@ -90,11 +90,11 @@ } "MatchUnpauseInfoMessage" { - "fr" "{1:N} a mis fin à la pause." + "fr" "{1} a mis fin à la pause." } "WaitingForUnpauseInfoMessage" { - "fr" "{1} désire reprendre le match, en attente que {2} tape !unpause" + "fr" "{1} désire reprendre le match, en attente que {2} tape {3}." } "PausesLeftInfoMessage" { @@ -126,11 +126,11 @@ } "ForceReadyInfoMessage" { - "fr" "Vous pouvez entrer {GREEN}!forceready {NORMAL}pour forcer le statut de votre équipe si vous avez moins de {GREEN}{1}{NORMAL} joueurs." + "fr" "Vous pouvez entrer {1} pour forcer le statut de votre équipe si vous avez moins de {2} joueurs." } "TeammateForceReadied" { - "fr" "Votre équipe s'est déclaré prête (de force) par {GREEN}{1}." + "fr" "Votre équipe s'est déclaré prête (de force) par {1}." } "AdminForceReadyInfoMessage" { @@ -148,21 +148,21 @@ { "fr" "Un admin a forcé la reprise du match." } - "TeamWantsToReloadLastRoundInfoMessage" + "TeamWantsToReloadCurrentRound" { - "fr" "{1} souhaite arrêter et recommencer au dernier round, cela nécessite que {2} confirme avec !stop." + "fr" "{1} souhaite arrêter et recommencer au dernier round, cela nécessite que {2} confirme avec {3}." } "TeamWinningSeriesInfoMessage" { - "fr" "{1}{NORMAL} gagne la série avec {2}-{3}" + "fr" "{1} gagne la série avec {2}-{3}." } "SeriesTiedInfoMessage" { - "fr" "La série est ex-æquo avec {1}-{2}" + "fr" "La série est ex-æquo avec {1}-{2}." } "NextSeriesMapInfoMessage" { - "fr" "La prochaine map dans la série est {GREEN}{1}" + "fr" "La prochaine map dans la série est {1}." } "TeamWonMatchInfoMessage" { @@ -172,10 +172,6 @@ { "fr" "{1} et {2} ont fait match nul." } - "TeamsSplitSeriesBO2InfoMessage" - { - "fr" "{1} et {2} ont gagné autant de matchs (1-1)." - } "TeamWonSeriesInfoMessage" { "fr" "{1} a remporté la série {2}-{3}." @@ -184,13 +180,9 @@ { "fr" "Le match est terminé" } - "CurrentScoreInfoMessage" - { - "fr" "{LIGHT_GREEN}{1} {GREEN}{2} {NORMAL}- {GREEN}{3} {LIGHT_GREEN}{4}" - } "BackupLoadedInfoMessage" { - "fr" "Sauvegarde chargée avec succès {1}" + "fr" "Sauvegarde chargée avec succès {1}." } "MatchBeginInSecondsInfoMessage" { @@ -222,7 +214,7 @@ } "ChangingMapInfoMessage" { - "fr" "Changement de map pour {GREEN}{1}…" + "fr" "Changement de map pour {1}..." } "MapDecidedInfoMessage" { @@ -230,19 +222,19 @@ } "MapIsInfoMessage" { - "fr" "Map {1} : {GREEN}{2}" + "fr" "Map {1}: {2}" } "TeamPickedMapInfoMessage" { - "fr" "{1} a choisi {GREEN}{2} {NORMAL}comme map {3}}" + "fr" "{1} a choisi {2} comme map {3}." } "TeamSelectSideInfoMessage" { - "fr" "{1} a choisi de commencer en {GREEN}{2} {NORMAL}sur {3}" + "fr" "{1} a choisi de commencer en {2} sur {3}." } "TeamVetoedMapInfoMessage" { - "fr" "{1} a rejeté {LIGHT_RED}{2}" + "fr" "{1} a rejeté {2}." } "CaptainLeftOnVetoInfoMessage" { @@ -250,7 +242,7 @@ } "ReadyToResumeVetoInfoMessage" { - "fr" "Entrez {GREEN}!ready {NORMAL}quand vous êtes prêt(e) à reprendre le tour de veto." + "fr" "Entrez {1} quand vous êtes prêt(e) à reprendre le tour de veto." } "MatchConfigLoadedInfoMessage" { @@ -268,10 +260,6 @@ { "fr" "[PAS PRÊT]" } - "MatchPoweredBy" - { - "fr" "Powered by {YELLOW}Get5" - } "MapVetoPickMenuText" { "fr" "Selectionnez une map à JOUER :" @@ -306,6 +294,6 @@ } "VetoCountdown" { - "fr" "Le veto commencera dans {GREEN}{1} {NORMAL}secondes." + "fr" "Le veto commencera dans {1} secondes." } } diff --git a/translations/get5.phrases.txt b/translations/get5.phrases.txt index 8fb79412b..87412ae7a 100644 --- a/translations/get5.phrases.txt +++ b/translations/get5.phrases.txt @@ -2,24 +2,28 @@ { "ReadyToVetoInfoMessage" { - "en" "Type {GREEN}!ready {NORMAL}when your team is ready to veto." + "#format" "{1:s}" + "en" "Type {1} when your team is ready to veto." } "WaitingForCastersReadyInfoMessage" { - "#format" "{1:s}" - "en" "Waiting for {1} to type {GREEN}!ready {NORMAL}to begin." + "#format" "{1:s},{2:s}" + "en" "Waiting for {1} to type {2} to begin." } "ReadyToRestoreBackupInfoMessage" { - "en" "Type {GREEN}!ready {NORMAL}when you are ready to restore the match backup." + "#format" "{1:s}" + "en" "Type {1} when you are ready to restore the match backup." } "ReadyToKnifeInfoMessage" { - "en" "Type {GREEN}!ready {NORMAL}when you are ready to knife." + "#format" "{1:s}" + "en" "Type {1} when you are ready to knife." } "ReadyToStartInfoMessage" { - "en" "Type {GREEN}!ready {NORMAL}when you are ready to begin." + "#format" "{1:s}" + "en" "Type {1} when you are ready to begin." } "YouAreReady" { @@ -27,7 +31,8 @@ } "YouAreReadyAuto" { - "en" "NOTE: You have been marked as ready due to game activity. Type !unready if you are not ready." + "#format" "{1:s}" + "en" "NOTE: You have been marked as ready due to game activity. Type {1} if you are not ready." } "YouAreNotReady" { @@ -35,8 +40,8 @@ } "WaitingForEnemySwapInfoMessage" { - "#format" "{1:s}" - "en" "{1} won the knife round. Waiting for them to type !stay or !swap." + "#format" "{1:s},{2:s},{3:s}" + "en" "{1} won the knife round. Waiting for them to type {2} or {3}." } "WaitingForGOTVBrodcastEndingInfoMessage" { @@ -56,7 +61,7 @@ } "TeamIsFullInfoMessage" { - "en" "Your team is full." + "en" "Your team is full" } "TeamForfeitInfoMessage" { @@ -90,12 +95,12 @@ } "MatchPausedByTeamMessage" { - "#format" "{1:N}" + "#format" "{1:s}" "en" "{1} has called for a tactical pause." } "MatchTechPausedByTeamMessage" { - "#format" "{1:N}" + "#format" "{1:s}" "en" "{1} has called for a technical pause." } "PausesNotEnabled" @@ -149,7 +154,8 @@ } "TechPauseRunoutInfoMessage" { - "en" "Maximum technical pause length has been reached. Anyone may unpause now." + "#format" "{1:s}" + "en" "Maximum technical pause length has been reached. Anyone may {1} now." } "TechPauseNoTimeRemaining" { @@ -168,13 +174,13 @@ } "MatchUnpauseInfoMessage" { - "#format" "{1:N}" + "#format" "{1:s}" "en" "{1} unpaused the match." } "WaitingForUnpauseInfoMessage" { - "#format" "{1:s},{2:s}" - "en" "{1} wants to unpause, waiting for {2} to type !unpause." + "#format" "{1:s},{2:s},{3:s}" + "en" "{1} wants to unpause, waiting for {2} to type {3}." } "PausesLeftInfoMessage" { @@ -223,13 +229,13 @@ } "ForceReadyInfoMessage" { - "#format" "{1:d}" - "en" "You may type {GREEN}!forceready {NORMAL}to force ready your team if you have less than {GREEN}{1}{NORMAL} players." + "#format" "{1:s},{2:s}" + "en" "You may type {1} to force-ready your team if you have less than {2} players." } "TeammateForceReadied" { - "#format" "{1:N}" - "en" "Your team was force-readied by {GREEN}{1}{NORMAL}." + "#format" "{1:s}" + "en" "Your team was force-readied by {1}." } "AdminForceReadyInfoMessage" { @@ -237,25 +243,30 @@ } "AdminForceEndInfoMessage" { - "en" "An admin force ended the match." + "en" "An admin force-ended the match." } "AdminForcePauseInfoMessage" { - "en" "An admin force paused the match." + "en" "An admin paused the match." + } + "AdminForceEndWithWinnerInfoMessage" + { + "#format" "{1:s}" + "en" "An admin force-ended the match, setting {1} as the winner." } "AdminForceUnPauseInfoMessage" { - "en" "An admin force unpaused the match." + "en" "An admin unpaused the match." } - "TeamWantsToReloadLastRoundInfoMessage" + "TeamWantsToReloadCurrentRound" { - "#format" "{1:s},{2:s}" - "en" "{1} wants to stop and reload last round, need {2} to confirm with !stop." + "#format" "{1:s},{2:s},{3:s}" + "en" "{1} wants to restore the game to the beginning of the current round. {2} must confirm with {3}." } "TeamWinningSeriesInfoMessage" { "#format" "{1:s},{2:d},{3:d}" - "en" "{1}{NORMAL} is winning the series {2}-{3}." + "en" "{1} is winning the series {2}-{3}." } "SeriesTiedInfoMessage" { @@ -264,8 +275,8 @@ } "NextSeriesMapInfoMessage" { - "#format" "{1:s}" - "en" "The next map in the series is {GREEN}{1}{NORMAL}." + "#format" "{1:s},{2:s}" + "en" "The next map in the series is {1} and it will start in {2}." } "TeamWonMatchInfoMessage" { @@ -277,11 +288,6 @@ "#format" "{1:s},{2:s}" "en" "{1} and {2} have tied the match." } - "TeamsSplitSeriesBO2InfoMessage" - { - "#format" "{1:s},{2:s}" - "en" "{1} and {2} have split the series 1-1." - } "TeamWonSeriesInfoMessage" { "#format" "{1:s},{2:d},{3:d}" @@ -291,15 +297,15 @@ { "en" "The match is over" } - "CurrentScoreInfoMessage" - { - "#format" "{1:s},{2:d},{3:d},{4:s}" - "en" "{LIGHT_GREEN}{1} {GREEN}{2} {NORMAL}- {GREEN}{3} {LIGHT_GREEN}{4}" - } "StopCommandNotEnabled" { "en" "The stop command is not enabled." } + "StopCommandVotingReset" + { + "#format" "{1:s}" + "en" "The request by {1} to stop the game was canceled as the round ended." + } "BackupLoadedInfoMessage" { "#format" "{1:s}" @@ -340,7 +346,7 @@ "ChangingMapInfoMessage" { "#format" "{1:s}" - "en" "Changing map to {GREEN}{1}..." + "en" "Changing map to {1}..." } "MapDecidedInfoMessage" { @@ -349,22 +355,22 @@ "MapIsInfoMessage" { "#format" "{1:d},{2:s}" - "en" "Map {1}: {GREEN}{2}{NORMAL}." + "en" "Map {1}: {2}" } "TeamPickedMapInfoMessage" { "#format" "{1:s},{2:s},{3:d}" - "en" "{1} picked {GREEN}{2} {NORMAL}as map {3}." + "en" "{1} picked {2} as map {3}." } "TeamSelectSideInfoMessage" { "#format" "{1:s},{2:s},{3:s}" - "en" "{1} has selected to start on {GREEN}{2} {NORMAL}on {3}." + "en" "{1} has selected to start on {2} on {3}." } "TeamVetoedMapInfoMessage" { "#format" "{1:s},{2:s}" - "en" "{1} vetoed {LIGHT_RED}{2}{NORMAL}." + "en" "{1} vetoed {2}." } "CaptainLeftOnVetoInfoMessage" { @@ -372,7 +378,8 @@ } "ReadyToResumeVetoInfoMessage" { - "en" "Type {GREEN}!ready {NORMAL}when you are ready to resume the veto." + "#format" "{1:s}" + "en" "Type {1} when you are ready to resume the veto." } "MatchConfigLoadedInfoMessage" { @@ -380,7 +387,30 @@ } "MoveToCoachInfoMessage" { - "en" "You were moved to the coach position because your team is full." + "en" "You were moved to the coach position as your team is full." + } + "CannotLeaveCoachingTeamIsFull" + { + "en" "You cannot leave the coach position as your team is full." + } + "CoachingNotEnabled" + { + "#format" "{1:s}" + "en" "Coaching is not enabled. You must set {1} to 1." + } + "PlayerIsCoachingTeam" + { + "#format" "{1:s},{2:s}" + "en" "{1} is coaching {2}." + } + "CanOnlyCoachDuringWarmup" + { + "en" "You can only change to or from coach during warmup." + } + "AllCoachSlotsFilledForTeam" + { + "#format" "{1:d}" + "en" "All coach slots ({1}) are currently filled for your team." } "ReadyTag" { @@ -390,10 +420,6 @@ { "en" "[NOT READY]" } - "MatchPoweredBy" - { - "en" "Powered by {YELLOW}Get5" - } "MapVetoPickMenuText" { "en" "Select a map to PLAY:" @@ -432,7 +458,7 @@ } "VetoCountdown" { - "#format" "{1:i}" - "en" "Veto commencing in {GREEN}{1} {NORMAL}seconds." + "#format" "{1:s}" + "en" "Veto commencing in {1} seconds." } } diff --git a/translations/hu/get5.phrases.txt b/translations/hu/get5.phrases.txt new file mode 100644 index 000000000..da23ee7ee --- /dev/null +++ b/translations/hu/get5.phrases.txt @@ -0,0 +1,375 @@ +"Phrases" +{ + "ReadyToVetoInfoMessage" + { + "hu" "Írd be a {1} parancsot, ha a csapatod készen áll a vétóra." + } + "WaitingForCastersReadyInfoMessage" + { + "hu" "A {1} várunk, hogy beírja a {2} parancsot a kezdéshez." + } + "ReadyToRestoreBackupInfoMessage" + { + "hu" "Írd be a {1} parancsot, ha a készen állsz a mérkőzés visszatöltésre." + } + "ReadyToKnifeInfoMessage" + { + "hu" "Írd be a {1} parancsot, ha készen állsz a késelésre." + } + "ReadyToStartInfoMessage" + { + "hu" "Írd be a {1} parancsot, ha készenállsz a kezdésre." + } + "YouAreReady" + { + "hu" "Készen állsz a mérkőzés megkezdésére." + } + "YouAreReadyAuto" + { + "hu" "INFO: Felkészült állapotba kerültél a játék aktívitásod által. Írd be, hogy {1} ha nem állsz még készen." + } + "YouAreNotReady" + { + "hu" "Nem állsz készen a mérkőzés megkezdésére." + } + "WaitingForEnemySwapInfoMessage" + { + "hu" "{1} nyerte meg a kés kört. Várakozunk, hogy kiválasszák a megfelelő oldalt. {2} vagy {3}." + } + "WaitingForGOTVBrodcastEndingInfoMessage" + { + "hu" "A pálya akkor fog váltani, ha a GOTV adás befejeződött." + } + "WaitingForGOTVVetoInfoMessage" + { + "hu" "A pálya akkor fog váltani, ha a GOTV adás megjelenítette a pálya vétókat." + } + "NoMatchSetupInfoMessage" + { + "hu" "Nincs mérkőzés előkészítve" + } + "YouAreNotAPlayerInfoMessage" + { + "hu" "Nem vagy játékos ezen a mérkőzésen" + } + "TeamIsFullInfoMessage" + { + "hu" "A csapatod megtelt" + } + "TeamForfeitInfoMessage" + { + "hu" "{1} nem sikerült időben felkészülni, ezért vesztettetek." + } + "MinutesToForfeitMessage" + { + "hu" "{1} {2} perc maradt, hogy felkészüljenek vagy elveszítik a mérkőzést." + } + "SecondsToForfeitInfoMessage" + { + "hu" "{1} {2} másodperc maradt, hogy felkészüljenek vagy elveszítik a mérkőzést." + } + "10SecondsToForfeitInfoMessage" + { + "hu" "{1} 10 másodperc maradt, hogy felkészüljenek vagy elveszítik a mérkőzést." + } + "MaxPausesUsedInfoMessage" + { + "hu" "A {2} felhasználta a maximális ({1}) időkérést." + } + "MaxPausesTimeUsedInfoMessage" + { + "hu" "A {2} felhasználta a maximális ({1}) időkérésre használható időt." + } + "MatchPausedByTeamMessage" + { + "hu" "{1} taktikai időkérést kért." + } + "MatchTechPausedByTeamMessage" + { + "hu" "{1} technikai időkérésért kért." + } + "PausesNotEnabled" + { + "hu" "Az időkérések nincsenek engedélyezve." + } + "TechPausesNotEnabled" + { + "hu" "A technikai időkérések nincsenek engedélyezve." + } + "TechnicalPauseMidSentence" + { + "hu" "technikai szünet" + } + "TacticalPauseMidSentence" + { + "hu" "taktikai szünet" + } + "TimeRemainingBeforeAnyoneCanUnpausePrefix" + { + "hu" "Ennyi idő maradt, mielőtt bárki elindíthatja a mérkőzést" + } + "PauseTimeRemainingPrefix" + { + "hu" "Hátralévő idő a szünetből" + } + "AwaitingUnpause" + { + "hu" "Várakozás a folytatásra" + } + "PausedByAdministrator" + { + "hu" "Egy admin leszünetelte a mérkőzést." + } + "PausedForBackup" + { + "hu" "A játék vissza lett töltve a kör mentésből. Mindkét csapatnak be kell írnia az {1} parancsot a folytatáshoz." + } + "UserCannotUnpauseAdmin" + { + "hu" "Egy admin kérte ezt az időkérést, ő is fogja tudni folytatni." + } + "PausingTeamCannotUnpauseUntilFreezeTime" + { + "hu" "Az időkérést, nem lehet visszavonni." + } + "PauseRunoutInfoMessage" + { + "hu" "{1} csapatnak, elfogyott a szüneteltetési ideje. Folytatódik a mérkőzés." + } + "TechPauseRunoutInfoMessage" + { + "hu" "A technikai szünetre szánt idő, elfogyott. Bárki tudja folytatni a mérkőést." + } + "TechPauseNoTimeRemaining" + { + "hu" "A {1} csapatnak, nem maradt több ideje technikai időkérésre. Használja a taktikai időkérést." + } + "TechPauseNoPausesRemaining" + { + "hu" "A {1} csapatnak, nem maradt több technikai időkérése. Használja a taktikai időkérést." + } + "TechPausePausesRemaining" + { + "hu" "Megmaradt technikai időkérések a {1} csapatnak: {2}" + } + "MatchUnpauseInfoMessage" + { + "hu" "{1} elindította a mérkőzést." + } + "WaitingForUnpauseInfoMessage" + { + "hu" "{1} szeretné folytatni. Várakozunk a {2} csapatra, hogy beírják a {3} parancsot." + } + "PausesLeftInfoMessage" + { + "hu" "Megmaradt taktikai időkérések a {1} csapatnak: {2}" + } + "PrereleaseVersionWarning" + { + "hu" "A Get5 ({1}) változatát futtatod, ami kifejezetten fejlesztésre és tesztelésre van. Ez az üzenet kikapcsolható a {2} parancs segítségével." + } + "NewVersionAvailable" + { + "hu" "Egy új változat elérhető a Get5-ból. Látogasd meg a {1} a frissítéssel kapcsolatban." + } + "TeamFailToReadyMinPlayerCheck" + { + "hu" "A szerveren legalább {1} játékosnak készen kell állnia a kezdéshez." + } + "TeamReadyToVetoInfoMessage" + { + "hu" "{1} készenáll a vétó megkezdésére." + } + "TeamReadyToRestoreBackupInfoMessage" + { + "hu" "{1} készen áll a mérkőzés visszaállítására." + } + "TeamReadyToKnifeInfoMessage" + { + "hu" "{1} készenáll, hogy késeljen az oldalért." + } + "TeamReadyToBeginInfoMessage" + { + "hu" "{1} készen áll a mérkőzés megkezdésére." + } + "TeamNotReadyInfoMessage" + { + "hu" "{1} már nem áll készen." + } + "ForceReadyInfoMessage" + { + "hu" "Beírhatod a {1} parancsot, ha fel szeretnéd készíteni a csapatod, ha kevesebb mint {2} játékosod van." + } + "TeammateForceReadied" + { + "hu" "A csapatod felkészült {1} által." + } + "AdminForceReadyInfoMessage" + { + "hu" "Egy admin, minden csapatot felkészített." + } + "AdminForceEndInfoMessage" + { + "hu" "Egy admin, véget vetett a mérkőzésnek." + } + "AdminForcePauseInfoMessage" + { + "hu" "Egy admin, szüneteli a mérkőzést." + } + "AdminForceUnPauseInfoMessage" + { + "hu" "Egy admin, a mérkőzés folytatását hajtotta végre." + } + "TeamWantsToReloadCurrentRound" + { + "hu" "{1} szeretne megállni és visszatölteni az előző kört. {2} el kell fogadni a {3} paranccsal." + } + "TeamWinningSeriesInfoMessage" + { + "hu" "{1} áll nyerésre {2}-{3}." + } + "SeriesTiedInfoMessage" + { + "hu" "Az állás döntetlen. {1}-{2}." + } + "NextSeriesMapInfoMessage" + { + "hu" "A következő pálya a szériában {1}." + } + "TeamWonMatchInfoMessage" + { + "hu" "{1} megnyerte a mérkőzést." + } + "TeamTiedMatchInfoMessage" + { + "hu" "{1} és a {2} döntetlent játszott." + } + "TeamWonSeriesInfoMessage" + { + "hu" "{1} nyerte a szériát. {2}-{3}." + } + "MatchFinishedInfoMessage" + { + "hu" "A mérkőzés befejeződött" + } + "StopCommandNotEnabled" + { + "hu" "A stop parancs nincs engedélyezve." + } + "BackupLoadedInfoMessage" + { + "hu" "Sikeresen visszatöltődött a körmentés {1}." + } + "MatchBeginInSecondsInfoMessage" + { + "hu" "A mérkőzés {1} másodperc múlva kezdődik." + } + "MatchIsLiveInfoMessage" + { + "hu" "A mérkőzés {GREEN}ÉLES" + } + "KnifeIn5SecInfoMessage" + { + "hu" "A kés kör 5 másodperc múlva kezdődik." + } + "KnifeInfoMessage" + { + "hu" "Kés!" + } + "TeamDecidedToStayInfoMessage" + { + "hu" "{1} úgy döntött, hogy maradnak." + } + "TeamDecidedToSwapInfoMessage" + { + "hu" "{1} úgy döntött, hogy térfelet cserélnek." + } + "TeamLostTimeToDecideInfoMessage" + { + "hu" "{1} maradnak, mivel idő letelte előtt, nem választottak." + } + "ChangingMapInfoMessage" + { + "hu" "Pályaváltás a következőre {1}..." + } + "MapDecidedInfoMessage" + { + "hu" "A pályák ellettek döntve:" + } + "MapIsInfoMessage" + { + "hu" "{1}. Pálya: {2}" + } + "TeamPickedMapInfoMessage" + { + "hu" "{1} választotta a {2} {3}. pályának." + } + "TeamSelectSideInfoMessage" + { + "hu" "{1} választott, hogy kezd {2} oldalon a(z) {3}. pályán." + } + "TeamVetoedMapInfoMessage" + { + "hu" "{1} szavazott {2}." + } + "CaptainLeftOnVetoInfoMessage" + { + "hu" "A csapatkapitány kilépett a vétó során, a vétó szünetel." + } + "ReadyToResumeVetoInfoMessage" + { + "hu" "Írd be a {1} parancsot ha készenállsz a vétó folytatására." + } + "MatchConfigLoadedInfoMessage" + { + "hu" "Mérkőzés beállítás betöltödött." + } + "MoveToCoachInfoMessage" + { + "hu" "Mivel a csapatod megtelt, így edzői poziccióba lettél átmozgatva." + } + "ReadyTag" + { + "hu" "[FELKÉSZÜLT]" + } + "NotReadyTag" + { + "hu" "[NEM ÁLL KÉSZEN]" + } + "MapVetoPickMenuText" + { + "hu" "Válassz pályát, amin játszanál:" + } + "MapVetoPickConfirmMenuText" + { + "hu" "Erősítse meg, hogy játszana {1}:" + } + "MapVetoBanMenuText" + { + "hu" "Válaszon egy pályát amit vétózna:" + } + "MapVetoBanConfirmMenuText" + { + "hu" "Erősítse meg, hogy vétózza a {1} pályát:" + } + "MapVetoSidePickMenuText" + { + "hu" "Válasszon oldalt {1}:" + } + "MapVetoSidePickConfirmMenuText" + { + "hu" "Erősítse meg, hogy {1}-ben kezd:" + } + "ConfirmPositiveOptionText" + { + "hu" "Igen" + } + "ConfirmNegativeOptionText" + { + "hu" "Nem" + } + "VetoCountdown" + { + "hu" "Kezdődik a vétózás {1} másodperc múlva." + } +} diff --git a/translations/pl/get5.phrases.txt b/translations/pl/get5.phrases.txt index 4fda81f07..26cc6d05a 100644 --- a/translations/pl/get5.phrases.txt +++ b/translations/pl/get5.phrases.txt @@ -2,27 +2,27 @@ { "ReadyToVetoInfoMessage" { - "pl" "Napisz {GREEN}!ready {NORMAL}kiedy Twoja drużyna będzie gotowa do głosowania." + "pl" "Napisz {1} kiedy Twoja drużyna będzie gotowa do głosowania." } "WaitingForCastersReadyInfoMessage" { - "pl" "Czekamy na komentatorów na wpisanie {GREEN}!ready {NORMAL}aby zacząć rozgrywkę." + "pl" "Czekamy na komentatorów na wpisanie {2} aby zacząć rozgrywkę." } "ReadyToRestoreBackupInfoMessage" { - "pl" "Napisz {GREEN}!ready {NORMAL}kiedy Twoja drużyna będzie gotowa do wczytania meczu." + "pl" "Napisz {1} kiedy Twoja drużyna będzie gotowa do wczytania meczu." } "ReadyToKnifeInfoMessage" { - "pl" "Napisz {GREEN}!ready {NORMAL}kiedy Twoja drużyna będzie gotowa do rundy nożowej." + "pl" "Napisz {1} kiedy Twoja drużyna będzie gotowa do rundy nożowej." } "ReadyToStartInfoMessage" { - "pl" "Napisz {GREEN}!ready {NORMAL}kiedy Twoja drużyna będzie gotowa." + "pl" "Napisz {1} kiedy Twoja drużyna będzie gotowa." } "WaitingForEnemySwapInfoMessage" { - "pl" "{1} wygralo runde nożową. Oczekiwanie na !stay lub !swap." + "pl" "{1} wygralo runde nożową. Oczekiwanie na {2} lub {3}." } "WaitingForGOTVBrodcastEndingInfoMessage" { @@ -74,11 +74,11 @@ } "MatchUnpauseInfoMessage" { - "pl" "{1:N} odpauzowało mecz." + "pl" "{1} odpauzowało mecz." } "WaitingForUnpauseInfoMessage" { - "pl" "{1} chce wznowić rozgrywkę, oczekiwanie na {2} na wpisanie !unpause." + "pl" "{1} chce wznowić rozgrywkę, oczekiwanie na {2} na wpisanie {3}." } "PausesLeftInfoMessage" { @@ -86,7 +86,7 @@ } "TeamFailToReadyMinPlayerCheck" { - "pl" "Musi być conajmniej {1}graczy na serwerze aby móc być gotowym." + "pl" "Musi być conajmniej {1} graczy na serwerze aby móc być gotowym." } "TeamReadyToVetoInfoMessage" { @@ -124,30 +124,26 @@ { "pl" "Administrator wznowił mecz." } - "TeamWantsToReloadLastRoundInfoMessage" + "TeamWantsToReloadCurrentRound" { - "pl" "{1} chce zatrzymać mecz i wczytać poprzednią rundę, oczekiwanie na potwierdzenie przez {2} i wpisanie !stop." + "pl" "{1} chce zatrzymać mecz i wczytać poprzednią rundę, oczekiwanie na potwierdzenie przez {2} i wpisanie {3}." } "TeamWinningSeriesInfoMessage" { - "pl" "{1}{NORMAL} wygrywa {2}-{3}" + "pl" "{1} wygrywa {2}-{3}." } "SeriesTiedInfoMessage" { - "pl" "Drużyny remisują {1}-{2}" + "pl" "Drużyny remisują {1}-{2}." } "NextSeriesMapInfoMessage" { - "pl" "Następna mapa w meczu to {GREEN}{1}" + "pl" "Następna mapa w meczu to {1}." } "TeamWonMatchInfoMessage" { "pl" "{1} wygrało mapę." } - "TeamsSplitSeriesBO2InfoMessage" - { - "pl" "{1} i {2} zremisowali spotkanie 1-1." - } "TeamWonSeriesInfoMessage" { "pl" "{1} wygrało spotkanie {2}-{3}." @@ -156,13 +152,9 @@ { "pl" "Mecz został zakończony" } - "CurrentScoreInfoMessage" - { - "pl" "{LIGHT_GREEN}{1} {GREEN}{2} {NORMAL}- {GREEN}{3} {LIGHT_GREEN}{4}" - } "BackupLoadedInfoMessage" { - "pl" "Pomyślnie wczytano kopię meczu {1}" + "pl" "Pomyślnie wczytano kopię meczu {1}." } "MatchBeginInSecondsInfoMessage" { @@ -194,7 +186,7 @@ } "ChangingMapInfoMessage" { - "pl" "Zmiana mapy na {GREEN}{1}..." + "pl" "Zmiana mapy na {1}..." } "MapDecidedInfoMessage" { @@ -202,19 +194,19 @@ } "MapIsInfoMessage" { - "pl" "Mapa {1}: {GREEN}{2}" + "pl" "Mapa {1}: {2}" } "TeamPickedMapInfoMessage" { - "pl" "{1} wybrało {GREEN}{2} {NORMAL}jako mapę {3}" + "pl" "{1} wybrało {2} jako mapę {3}." } "TeamSelectSideInfoMessage" { - "pl" "{1} wybrało stronę {GREEN}{2} {NORMAL}na {3}" + "pl" "{1} wybrało stronę {2} na {3}." } "TeamVetoedMapInfoMessage" { - "pl" "{1} odrzuciło {LIGHT_RED}{2}" + "pl" "{1} odrzuciło {2}." } "CaptainLeftOnVetoInfoMessage" { @@ -222,7 +214,7 @@ } "ReadyToResumeVetoInfoMessage" { - "pl" "Napisz {GREEN}!ready {NORMAL}kiedy Twoja drużyna będzie gotowa do wznowienia głosowania." + "pl" "Napisz {1} kiedy Twoja drużyna będzie gotowa do wznowienia głosowania." } "MatchConfigLoadedInfoMessage" { diff --git a/translations/pt/get5.phrases.txt b/translations/pt/get5.phrases.txt index 1f00fe1fc..615cb1ffa 100644 --- a/translations/pt/get5.phrases.txt +++ b/translations/pt/get5.phrases.txt @@ -2,35 +2,35 @@ { "ReadyToVetoInfoMessage" { - "pt" "Digite {GREEN}!ready {NORMAL}quando seu time estiver pronto para o veto." + "pt" "Digite {1} quando seu time estiver pronto para o veto." } "WaitingForCastersReadyInfoMessage" { - "pt" "Esperando pelos streammers para digitar {GREEN}!ready {NORMAL}pra começar." + "pt" "Esperando pelos streammers para digitar {2} pra começar." } "ReadyToRestoreBackupInfoMessage" { - "pt" "Digite {GREEN}!ready {NORMAL}quando vocês estiver pronto para restaurar o backup da partida." + "pt" "Digite {1} quando vocês estiver pronto para restaurar o backup da partida." } "ReadyToKnifeInfoMessage" { - "pt" "Digite {GREEN}!ready {NORMAL}quando você estiver pronto pro Round Faca." + "pt" "Digite {1} quando você estiver pronto pro Round Faca." } "ReadyToStartInfoMessage" { - "pt" "Digite {GREEN}!ready {NORMAL}quando você estiver pronto para começar." + "pt" "Digite {1} quando você estiver pronto para começar." } "YouAreReady" { - "pt" "Você foi marcado como {GREEN}pronto{NORMAL}." + "pt" "Você foi marcado como pronto." } "YouAreNotReady" { - "pt" "Você foi desmarcado como {GREEN}pronto{NORMAL}." + "pt" "Você foi desmarcado como pronto." } "WaitingForEnemySwapInfoMessage" { - "pt" "{1} venceu o Round Faca. Esperando pelo time vencedor digitar !stay ou !swap." + "pt" "{1} venceu o Round Faca. Esperando pelo time vencedor digitar {2} ou {3}." } "WaitingForGOTVBrodcastEndingInfoMessage" { @@ -82,11 +82,11 @@ } "MatchUnpauseInfoMessage" { - "pt" "{1:N} resumiu a partida." + "pt" "{1} resumiu a partida." } "WaitingForUnpauseInfoMessage" { - "pt" "{1} quer resumir a partida, mas esperando por {2} digitar !unpause." + "pt" "{1} quer resumir a partida, mas esperando por {2} digitar {3}." } "PausesLeftInfoMessage" { @@ -118,11 +118,11 @@ } "ForceReadyInfoMessage" { - "pt" "Você pode digitar {GREEN}!forceready {NORMAL}para forçar seu time a ficar pronto se você tiver menos de {GREEN}{1}{NORMAL} jogador." + "pt" "Você pode digitar {1} para forçar seu time a ficar pronto se você tiver menos de {2} jogador." } "TeammateForceReadied" { - "pt" "Seu time foi forçado a ficar pronto pelo jogador {GREEN}{1}" + "pt" "Seu time foi forçado a ficar pronto pelo jogador {1}." } "AdminForceReadyInfoMessage" { @@ -140,45 +140,37 @@ { "pt" "O administrador forçou o resumo de uma partida." } - "TeamWantsToReloadLastRoundInfoMessage" + "TeamWantsToReloadCurrentRound" { - "pt" "{1} quer parar e recomeçar o último round. Para isso precisa que outros {2} confirmem digitanto !stop." + "pt" "{1} quer parar e recomeçar o último round. Para isso precisa que outros {2} confirmem digitanto {3}." } "TeamWinningSeriesInfoMessage" { - "pt" "{1}{NORMAL} está vencendo a série {2}-{3}" + "pt" "{1} está vencendo a série {2}-{3}." } "SeriesTiedInfoMessage" { - "pt" "A série está empatada em {1}-{2}" + "pt" "A série está empatada em {1}-{2}." } "NextSeriesMapInfoMessage" { - "pt" "O próximo mapa da série será {GREEN}{1}" + "pt" "O próximo mapa da série será {1}." } "TeamWonMatchInfoMessage" { "pt" "{1} é a equipe vencedora da partida." } - "TeamsSplitSeriesBO2InfoMessage" - { - "pt" "{1} e {2} empataram a série em 1-1." - } "TeamWonSeriesInfoMessage" { "pt" "{1} é a equipe vencedora da série {2}-{3}." } "MatchFinishedInfoMessage" { - "pt" "A partida foi fnializada." - } - "CurrentScoreInfoMessage" - { - "pt" "{LIGHT_GREEN}{1} {GREEN}{2} {NORMAL}- {GREEN}{3} {LIGHT_GREEN}{4}" + "pt" "A partida foi fnializada" } "BackupLoadedInfoMessage" { - "pt" "Backup carregado com sucesso {1}" + "pt" "Backup carregado com sucesso {1}." } "MatchBeginInSecondsInfoMessage" { @@ -186,7 +178,7 @@ } "MatchIsLiveInfoMessage" { - "pt" "{GREEN}Começou a partida! Boa diversão e boa sorte!" + "pt" "A partida é {GREEN}LIVE" } "KnifeIn5SecInfoMessage" { @@ -210,7 +202,7 @@ } "ChangingMapInfoMessage" { - "pt" "Mudando o mapa para {GREEN}{1}..." + "pt" "Mudando o mapa para {1}..." } "MapDecidedInfoMessage" { @@ -218,19 +210,19 @@ } "MapIsInfoMessage" { - "pt" "Mapa {1}: {GREEN}{2}" + "pt" "Mapa {1}: {2}" } "TeamPickedMapInfoMessage" { - "pt" "{1} escolheu {GREEN}{2} {NORMAL}como mapa {3}" + "pt" "{1} escolheu {2} como mapa {3}." } "TeamSelectSideInfoMessage" { - "pt" "{1} escolheu começar no lado {GREEN}{2} {NORMAL}em {3}" + "pt" "{1} escolheu começar no lado {2} em {3}." } "TeamVetoedMapInfoMessage" { - "pt" "{1} vetou {LIGHT_RED}{2}" + "pt" "{1} vetou {2}." } "CaptainLeftOnVetoInfoMessage" { @@ -238,7 +230,7 @@ } "ReadyToResumeVetoInfoMessage" { - "pt" "Digite {GREEN}!ready {NORMAL}quando você estiver pronto para resumir o veto." + "pt" "Digite {1} quando você estiver pronto para resumir o veto." } "MatchConfigLoadedInfoMessage" { @@ -256,8 +248,4 @@ { "pt" "[NOT READY]" } - "MatchPoweredBy" - { - "pt" "Oferecido por {YELLOW}Get5" - } } diff --git a/translations/ru/get5.phrases.txt b/translations/ru/get5.phrases.txt index 94925465d..136319899 100644 --- a/translations/ru/get5.phrases.txt +++ b/translations/ru/get5.phrases.txt @@ -2,27 +2,27 @@ { "ReadyToVetoInfoMessage" { - "ru" "Напишите {GREEN}!ready {NORMAL}когда ваша команда будет готова к голосованию." + "ru" "Напишите {1} когда ваша команда будет готова к голосованию." } "WaitingForCastersReadyInfoMessage" { - "ru" "Ожидаем, пока все игроки напишут {GREEN}!ready {NORMAL}для начала." + "ru" "Ожидаем, пока все игроки напишут {2} для начала." } "ReadyToRestoreBackupInfoMessage" { - "ru" "Напиши {GREEN}!ready {NORMAL}когда твоя команда будет готова загрузить сохраненную игру." + "ru" "Напиши {1} когда твоя команда будет готова загрузить сохраненную игру." } "ReadyToKnifeInfoMessage" { - "ru" "Напиши {GREEN}!ready {NORMAL}когда твоя команда будет готова к ножевому раунду." + "ru" "Напиши {1} когда твоя команда будет готова к ножевому раунду." } "ReadyToStartInfoMessage" { - "ru" "Напиши {GREEN}!ready {NORMAL}когда твоя команда будет готова начать." + "ru" "Напиши {1} когда твоя команда будет готова начать." } "WaitingForEnemySwapInfoMessage" { - "ru" "{1} выиграла раунд. Ожидаем, пока они напишут !stay или !swap." + "ru" "{1} выиграла раунд. Ожидаем, пока они напишут {2} или {3}." } "WaitingForGOTVBrodcastEndingInfoMessage" { @@ -78,7 +78,7 @@ } "WaitingForUnpauseInfoMessage" { - "ru" "{1} желает снять паузу, ожидаем когда {2} напишут !unpause." + "ru" "{1} желает снять паузу, ожидаем когда {2} напишут {3}." } "TeamReadyToVetoInfoMessage" { @@ -116,30 +116,26 @@ { "ru" "Администратор силы возобновленная игра." } - "TeamWantsToReloadLastRoundInfoMessage" + "TeamWantsToReloadCurrentRound" { - "ru" "{1} желает остановить и перезагрузить последний раунд, ждем {2} чтобы написали !stop для подтверждения." + "ru" "{1} желает остановить и перезагрузить последний раунд, ждем {2} чтобы написали {3} для подтверждения." } "TeamWinningSeriesInfoMessage" { - "ru" "{1}{NORMAL} выигрывает со счетом {2}-{3}" + "ru" "{1} выигрывает со счетом {2}-{3}." } "SeriesTiedInfoMessage" { - "ru" "Матч остановлен на счете {1}-{2}" + "ru" "Матч остановлен на счете {1}-{2}." } "NextSeriesMapInfoMessage" { - "ru" "Следующая карта в серии {GREEN}{1}" + "ru" "Следующая карта в серии {1}." } "TeamWonMatchInfoMessage" { "ru" "{1} выиграл матч." } - "TeamsSplitSeriesBO2InfoMessage" - { - "ru" "{1} и {2} закончили серию со счетом 1-1." - } "TeamWonSeriesInfoMessage" { "ru" "{1} выиграли серию {2}-{3}." @@ -148,13 +144,9 @@ { "ru" "Матч уже завершен" } - "CurrentScoreInfoMessage" - { - "ru" "{LIGHT_GREEN}{1} {GREEN}{2} {NORMAL}- {GREEN}{3} {LIGHT_GREEN}{4}" - } "BackupLoadedInfoMessage" { - "ru" "Удачно загружен бэкап {1}" + "ru" "Удачно загружен бэкап {1}." } "MatchBeginInSecondsInfoMessage" { @@ -162,7 +154,7 @@ } "MatchIsLiveInfoMessage" { - "ru" "Матч {GREEN}НАЧАЛСЯ!" + "ru" "Матч {GREEN}НАЧАЛСЯ" } "KnifeIn5SecInfoMessage" { @@ -186,7 +178,7 @@ } "ChangingMapInfoMessage" { - "ru" "Карта изменена на {GREEN}{1}..." + "ru" "Карта изменена на {1}..." } "MapDecidedInfoMessage" { @@ -194,19 +186,19 @@ } "MapIsInfoMessage" { - "ru" "Карта {1}: {GREEN}{2}" + "ru" "Карта {1}: {2}" } "TeamPickedMapInfoMessage" { - "ru" "{1} выбрал {GREEN}{2} {NORMAL}как карту {3}" + "ru" "{1} выбрал {2} как карту {3}." } "TeamSelectSideInfoMessage" { - "ru" "{1} выбрали начать за {GREEN}{2} {NORMAL}на {3}" + "ru" "{1} выбрали начать за {2} на {3}." } "TeamVetoedMapInfoMessage" { - "ru" "{1} вычеркнули {LIGHT_RED}{2}" + "ru" "{1} вычеркнули {2}." } "CaptainLeftOnVetoInfoMessage" { @@ -214,7 +206,7 @@ } "ReadyToResumeVetoInfoMessage" { - "ru" "Напишите {GREEN}!ready {NORMAL}, когда будете готовы продолжить голосование." + "ru" "Напишите {1}, когда будете готовы продолжить голосование." } "MatchConfigLoadedInfoMessage" {