From: Lyberta Date: Mon, 3 Sep 2018 18:13:11 +0000 (+0300) Subject: Merge branch 'master' into Lyberta/TeamplayOverhaul2 X-Git-Tag: xonotic-v0.8.5~1899^2~2 X-Git-Url: http://de.git.xonotic.org/?p=xonotic%2Fxonotic-data.pk3dir.git;a=commitdiff_plain;h=9c24e122b771b8afb620a1f7db98b2d6f38b04af;hp=ba7c5c7aa1351282377f6c4afc4653a130409255 Merge branch 'master' into Lyberta/TeamplayOverhaul2 --- diff --git a/notifications.cfg b/notifications.cfg index 143b5c0e2..11a42fc9a 100644 --- a/notifications.cfg +++ b/notifications.cfg @@ -261,7 +261,7 @@ seta notification_INFO_POWERUP_STRENGTH "1" "0 = off, 1 = print to console, 2 = seta notification_INFO_QUIT_DISCONNECT "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_QUIT_KICK_IDLING "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_QUIT_KICK_SPECTATING "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" -seta notification_INFO_QUIT_SPECTATE "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" +seta notification_INFO_QUIT_SPECTATE "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_RACE_ABANDONED "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_RACE_FAIL_RANKED "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_RACE_FAIL_UNRANKED "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" diff --git a/qcsrc/common/minigames/sv_minigames.qc b/qcsrc/common/minigames/sv_minigames.qc index 5c6af2661..af74e6a5a 100644 --- a/qcsrc/common/minigames/sv_minigames.qc +++ b/qcsrc/common/minigames/sv_minigames.qc @@ -9,7 +9,7 @@ void player_clear_minigame(entity player) set_movetype(player, MOVETYPE_WALK); else set_movetype(player, MOVETYPE_FLY_WORLDONLY); - player.team_forced = 0; + Player_SetForcedTeamIndex(player, TEAM_FORCE_DEFAULT); } void minigame_rmplayer(entity minigame_session, entity player) @@ -150,7 +150,7 @@ int minigame_addplayer(entity minigame_session, entity player) PutObserverInServer(player); } if ( autocvar_sv_minigames_observer == 2 ) - player.team_forced = -1; + Player_SetForcedTeamIndex(player, TEAM_FORCE_SPECTATOR); minigame_resend(minigame_session); } diff --git a/qcsrc/common/notifications/all.inc b/qcsrc/common/notifications/all.inc index db0e0503f..9c352c4e1 100644 --- a/qcsrc/common/notifications/all.inc +++ b/qcsrc/common/notifications/all.inc @@ -422,7 +422,7 @@ MSG_INFO_NOTIF(QUIT_KICK_IDLING, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 was kicked for idling"), "") MSG_INFO_NOTIF(QUIT_KICK_SPECTATING, N_CONSOLE, 0, 0, "", "", "", _("^F2You were kicked from the server because you are a spectator and spectators aren't allowed at the moment."), "") MSG_INFO_NOTIF(QUIT_KICK_TEAMKILL, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 was kicked for excessive teamkilling"), "") - MSG_INFO_NOTIF(QUIT_SPECTATE, N_CONSOLE, 1, 0, "s1", "", "", _("^BG%s^F3 is now spectating"), "") + MSG_INFO_NOTIF(QUIT_SPECTATE, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 is now spectating"), "") MSG_INFO_NOTIF(RACE_ABANDONED, N_CONSOLE, 1, 0, "s1", "", "", _("^BG%s^BG has abandoned the race"), "") MSG_INFO_NOTIF(RACE_FAIL_RANKED, N_CONSOLE, 1, 3, "s1 race_col f1ord race_col f3race_time race_diff", "s1 f3race_time", "race_newfail", _("^BG%s^BG couldn't break their %s%s^BG place record of %s%s %s"), "") diff --git a/qcsrc/server/autocvars.qh b/qcsrc/server/autocvars.qh index 7d73a73a8..8a19f06c6 100644 --- a/qcsrc/server/autocvars.qh +++ b/qcsrc/server/autocvars.qh @@ -88,9 +88,6 @@ float autocvar_g_balance_powerup_strength_selfforce; //float autocvar_g_balance_powerup_strength_time; float autocvar_g_balance_superweapons_time; float autocvar_g_balance_selfdamagepercent; -bool autocvar_g_balance_teams; -bool autocvar_g_balance_teams_prevent_imbalance; -//float autocvar_g_balance_teams_scorefactor; float autocvar_g_ballistics_density_corpse; float autocvar_g_ballistics_density_player; float autocvar_g_ballistics_mindistance; @@ -109,7 +106,6 @@ bool autocvar_g_campaign; #define autocvar_g_campaign_forceteam cvar("g_campaign_forceteam") int autocvar_g_campaign_skill; int autocvar_g_casings; -bool autocvar_g_changeteam_banned; float autocvar_g_chat_flood_burst; float autocvar_g_chat_flood_burst_team; float autocvar_g_chat_flood_burst_tell; @@ -124,11 +120,7 @@ int autocvar_g_chat_nospectators; bool autocvar_g_chat_teamcolors; bool autocvar_g_chat_tellprivacy; bool autocvar_g_forced_respawn; -string autocvar_g_forced_team_blue; -string autocvar_g_forced_team_otherwise; -string autocvar_g_forced_team_pink; -string autocvar_g_forced_team_red; -string autocvar_g_forced_team_yellow; +string autocvar_g_forced_team_otherwise; // TODO: Move to teamplay.qc #define autocvar_g_friendlyfire cvar("g_friendlyfire") #define autocvar_g_friendlyfire_virtual cvar("g_friendlyfire_virtual") #define autocvar_g_friendlyfire_virtual_force cvar("g_friendlyfire_virtual_force") @@ -366,8 +358,6 @@ bool autocvar_sv_vote_gamestart; string autocvar_sv_weaponstats_file; float autocvar_sv_gibhealth; float autocvar_sys_ticrate; -bool autocvar_teamplay_lockonrestart; -int autocvar_teamplay_mode; #define autocvar_timelimit cvar("timelimit") #define autocvar_timelimit_override cvar("timelimit_override") float autocvar_timelimit_increment; diff --git a/qcsrc/server/client.qc b/qcsrc/server/client.qc index 85514bdf9..ad7f8c47c 100644 --- a/qcsrc/server/client.qc +++ b/qcsrc/server/client.qc @@ -285,25 +285,16 @@ void PutObserverInServer(entity this) WaypointSprite_PlayerDead(this); - if (mutator_returnvalue) { - // mutator prevents resetting teams+score - } else { - Player_SetTeamIndex(this, -1); - this.frags = FRAGS_SPECTATOR; - PlayerScore_Clear(this); // clear scores when needed - } - if (CS(this).killcount != FRAGS_SPECTATOR) { - Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_SPECTATE, this.netname); if(!game_stopped) if(autocvar_g_chat_nospectators == 1 || (!warmup_stage && autocvar_g_chat_nospectators == 2)) Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_CHAT_NOSPECTATORS); - if(!CS(this).just_joined) - LogTeamchange(this.playerid, -1, TEAM_CHANGE_SPECTATOR); - else + if(CS(this).just_joined) + { CS(this).just_joined = false; + } } accuracy_resend(this); @@ -388,6 +379,16 @@ void PutObserverInServer(entity this) if(axh.owner == this && axh != NULL && !wasfreed(axh)) delete(axh); } + + if (mutator_returnvalue) + { + // mutator prevents resetting teams+score + } + else + { + SetPlayerTeam(this, -1, TEAM_CHANGE_SPECTATOR); + this.frags = FRAGS_SPECTATOR; + } } int player_getspecies(entity this) @@ -1070,38 +1071,7 @@ void ClientConnect(entity this) bot_clientconnect(this); - // identify the right forced team - if (autocvar_g_campaign) - { - if (IS_REAL_CLIENT(this)) // only players, not bots - { - switch (autocvar_g_campaign_forceteam) - { - case 1: this.team_forced = NUM_TEAM_1; break; - case 2: this.team_forced = NUM_TEAM_2; break; - case 3: this.team_forced = NUM_TEAM_3; break; - case 4: this.team_forced = NUM_TEAM_4; break; - default: this.team_forced = 0; - } - } - } - else if (PlayerInList(this, autocvar_g_forced_team_red)) this.team_forced = NUM_TEAM_1; - else if (PlayerInList(this, autocvar_g_forced_team_blue)) this.team_forced = NUM_TEAM_2; - else if (PlayerInList(this, autocvar_g_forced_team_yellow)) this.team_forced = NUM_TEAM_3; - else if (PlayerInList(this, autocvar_g_forced_team_pink)) this.team_forced = NUM_TEAM_4; - else switch (autocvar_g_forced_team_otherwise) - { - default: this.team_forced = 0; break; - case "red": this.team_forced = NUM_TEAM_1; break; - case "blue": this.team_forced = NUM_TEAM_2; break; - case "yellow": this.team_forced = NUM_TEAM_3; break; - case "pink": this.team_forced = NUM_TEAM_4; break; - case "spectate": - case "spectator": - this.team_forced = -1; - break; - } - if (!teamplay && this.team_forced > 0) this.team_forced = 0; + Player_DetermineForcedTeam(this); TRANSMUTE(Observer, this); @@ -1905,7 +1875,7 @@ void ShowRespawnCountdown(entity this) .bool team_selected; bool ShowTeamSelection(entity this) { - if(!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || this.team_selected || (CS(this).wasplayer && autocvar_g_changeteam_banned) || this.team_forced > 0) + if (!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || this.team_selected || (CS(this).wasplayer && autocvar_g_changeteam_banned) || Player_HasRealForcedTeam(this)) return false; stuffcmd(this, "menu_showteamselect\n"); return true; @@ -1928,7 +1898,6 @@ void Join(entity this) if(IS_PLAYER(this)) if(teamplay && this.team != -1) { - //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_PLAY_TEAM), this.netname); } else Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname); @@ -1953,7 +1922,7 @@ int nJoinAllowed(entity this, entity ignore) return 0; } - if(this && this.team_forced < 0) + if(this && (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR)) return 0; // forced spectators can never join // TODO simplify this @@ -2505,7 +2474,7 @@ void PlayerPreThink (entity this) // don't do this in ClientConnect // many things can go wrong if a client is spawned as player on connection if (MUTATOR_CALLHOOK(AutoJoinOnConnection, this) - || (!(autocvar_sv_spectate || autocvar_g_campaign || this.team_forced < 0) + || (!(autocvar_sv_spectate || autocvar_g_campaign || (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR)) && (!teamplay || autocvar_g_balance_teams))) { campaign_bots_may_start = true; diff --git a/qcsrc/server/client.qh b/qcsrc/server/client.qh index efb61a436..099fac9f5 100644 --- a/qcsrc/server/client.qh +++ b/qcsrc/server/client.qh @@ -226,6 +226,8 @@ METHOD(Client, m_unwind, bool(Client this)) return false; } +bool PlayerInList(entity player, string list); + /// \brief Print the string to the client's chat. /// \param[in] client Client to print to. /// \param[in] text Text to print. diff --git a/qcsrc/server/clientkill.qc b/qcsrc/server/clientkill.qc index 0bb761483..8d7293bb0 100644 --- a/qcsrc/server/clientkill.qc +++ b/qcsrc/server/clientkill.qc @@ -22,7 +22,10 @@ void ClientKill_Now_TeamChange(entity this) PutObserverInServer(this); } else - SV_ChangeTeam(this, this.killindicator_teamchange - 1); + { + Player_SetTeamIndexChecked(this, Team_TeamToIndex( + this.killindicator_teamchange)); + } this.killindicator_teamchange = 0; } diff --git a/qcsrc/server/command/cmd.qc b/qcsrc/server/command/cmd.qc index 353d81395..6f1d60dd9 100644 --- a/qcsrc/server/command/cmd.qc +++ b/qcsrc/server/command/cmd.qc @@ -119,7 +119,7 @@ void ClientCommand_clientversion(entity caller, int request, int argc) // inter { // JoinBestTeam(caller, false, true); } - else if (teamplay && !autocvar_sv_spectate && !(caller.team_forced > 0)) + else if (teamplay && !autocvar_sv_spectate && !(Player_GetForcedTeamIndex(caller) > 0)) { TRANSMUTE(Observer, caller); // really? stuffcmd(caller, "menu_showteamselect\n"); @@ -352,7 +352,7 @@ void ClientCommand_selectteam(entity caller, int request, int argc) sprint(caller, "^7selectteam can only be used in teamgames\n"); return; } - if (caller.team_forced > 0) + if (Player_GetForcedTeamIndex(caller) > 0) { sprint(caller, "^7selectteam can not be used as your team is forced\n"); return; diff --git a/qcsrc/server/command/sv_cmd.qc b/qcsrc/server/command/sv_cmd.qc index c917874c3..c3e5bf901 100644 --- a/qcsrc/server/command/sv_cmd.qc +++ b/qcsrc/server/command/sv_cmd.qc @@ -1052,8 +1052,8 @@ void GameCommand_moveplayer(int request, int argc) { // set up float team_id; - float save = client.team_forced; - client.team_forced = 0; + int save = Player_GetForcedTeamIndex(client); + Player_SetForcedTeamIndex(client, TEAM_FORCE_DEFAULT); // find the team to move the player to team_id = Team_ColorToTeam(destination); @@ -1073,7 +1073,7 @@ void GameCommand_moveplayer(int request, int argc) { balance = TeamBalance_CheckAllowedTeams(client); } - client.team_forced = save; + Player_SetForcedTeamIndex(client, save); // Check to see if the destination team is even available switch (team_id) @@ -1130,7 +1130,7 @@ void GameCommand_moveplayer(int request, int argc) } // If so, lets continue and finally move the player - client.team_forced = 0; + Player_SetForcedTeamIndex(client, TEAM_FORCE_DEFAULT); if (MoveToTeam(client, Team_TeamToIndex(team_id), 6)) { successful = strcat(successful, (successful ? ", " : ""), playername(client, false)); @@ -1312,7 +1312,7 @@ void GameCommand_shuffleteams(int request) } FOREACH_CLIENT(IS_PLAYER(it) || it.caplayer, { - if (it.team_forced) { + if (Player_HasRealForcedTeam(it)) { // we could theoretically assign forced players to their teams // and shuffle the rest to fill the empty spots but in practise // either all players or none are gonna have forced teams diff --git a/qcsrc/server/defs.qh b/qcsrc/server/defs.qh index 173c2d5ca..58111fb12 100644 --- a/qcsrc/server/defs.qh +++ b/qcsrc/server/defs.qh @@ -351,8 +351,6 @@ const int ACTIVE_TOGGLE = 3; //float serverflags; -.int team_forced; // can be a team number to force a team, or 0 for default action, or -1 for forced spectator - .bool player_blocked; .float revival_time; // time at which player was last revived diff --git a/qcsrc/server/g_damage.qc b/qcsrc/server/g_damage.qc index a25ae5bec..4964bf91e 100644 --- a/qcsrc/server/g_damage.qc +++ b/qcsrc/server/g_damage.qc @@ -4,6 +4,7 @@ #include "bot/api.qh" #include "g_hook.qh" #include +#include "teamplay.qh" #include "scores.qh" #include "spawnpoints.qh" #include "../common/state.qh" diff --git a/qcsrc/server/teamplay.qc b/qcsrc/server/teamplay.qc index 2c41ae18b..b373d9f15 100644 --- a/qcsrc/server/teamplay.qc +++ b/qcsrc/server/teamplay.qc @@ -28,6 +28,8 @@ enum /// \brief Indicates that the player is not allowed to join a team. const int TEAM_NOT_ALLOWED = -1; +.float team_forced; // can be a team number to force a team, or 0 for default action, or -1 for forced spectator + .int m_team_balance_state; ///< Holds the state of the team balance entity. .entity m_team_balance_team[NUM_TEAMS]; ///< ??? @@ -37,6 +39,11 @@ const int TEAM_NOT_ALLOWED = -1; .int m_num_players_alive; ///< Number of alive players in a team. .int m_num_control_points; ///< Number of control points owned by a team. +string autocvar_g_forced_team_red; +string autocvar_g_forced_team_blue; +string autocvar_g_forced_team_yellow; +string autocvar_g_forced_team_pink; + entity g_team_entities[NUM_TEAMS]; ///< Holds global team entities. STATIC_INIT(g_team_entities) @@ -204,22 +211,85 @@ bool SetPlayerTeam(entity player, int team_index, int type) { return false; } - LogTeamchange(player.playerid, player.team, type); + LogTeamChange(player.playerid, player.team, type); if (team_index != old_team_index) { - Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(player.team, - INFO_JOIN_PLAY_TEAM), player.netname); + PlayerScore_Clear(player); + if (team_index != -1) + { + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM( + player.team, INFO_JOIN_PLAY_TEAM), player.netname); + } + else + { + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_SPECTATE, + player.netname); + } KillPlayerForTeamChange(player); + if (!IS_BOT_CLIENT(player)) + { + TeamBalance_AutoBalanceBots(); + } } return true; } +void Player_SetTeamIndexChecked(entity player, int team_index) +{ + if (!teamplay) + { + return; + } + if (!Team_IsValidIndex(team_index)) + { + return; + } + if ((autocvar_g_campaign) || (autocvar_g_changeteam_banned && + CS(player).wasplayer)) + { + Send_Notification(NOTIF_ONE, player, MSG_INFO, + INFO_TEAMCHANGE_NOTALLOWED); + return; + } + entity balance = TeamBalance_CheckAllowedTeams(player); + if (team_index == 1 && !TeamBalance_IsTeamAllowedInternal(balance, 1)) + { + team_index = 4; + } + if (team_index == 4 && !TeamBalance_IsTeamAllowedInternal(balance, 4)) + { + team_index = 3; + } + if (team_index == 3 && !TeamBalance_IsTeamAllowedInternal(balance, 3)) + { + team_index = 2; + } + if (team_index == 2 && !TeamBalance_IsTeamAllowedInternal(balance, 2)) + { + team_index = 1; + } + // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless + if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance) + { + TeamBalance_GetTeamCounts(balance, player); + if ((Team_IndexToBit(team_index) & TeamBalance_FindBestTeams(balance, + player, false)) == 0) + { + Send_Notification(NOTIF_ONE, player, MSG_INFO, + INFO_TEAMCHANGE_LARGERTEAM); + TeamBalance_Destroy(balance); + return; + } + } + TeamBalance_Destroy(balance); + SetPlayerTeam(player, team_index, TEAM_CHANGE_MANUAL); +} + bool MoveToTeam(entity client, int team_index, int type) { //PrintToChatAll(sprintf("MoveToTeam: %s, %f", client.netname, team_index)); int lockteams_backup = lockteams; // backup any team lock lockteams = 0; // disable locked teams - PlayerScore_Clear(client); if (!SetPlayerTeam(client, team_index, type)) { lockteams = lockteams_backup; // restore the team lock @@ -229,29 +299,150 @@ bool MoveToTeam(entity client, int team_index, int type) return true; } -void KillPlayerForTeamChange(entity player) +bool Player_HasRealForcedTeam(entity player) { - if (IS_DEAD(player)) + return player.team_forced > TEAM_FORCE_DEFAULT; +} + +int Player_GetForcedTeamIndex(entity player) +{ + return player.team_forced; +} + +void Player_SetForcedTeamIndex(entity player, int team_index) +{ + switch (team_index) { - return; + case TEAM_FORCE_SPECTATOR: + case TEAM_FORCE_DEFAULT: + { + player.team_forced = team_index; + break; + } + default: + { + if (!Team_IsValidIndex(team_index)) + { + LOG_FATAL("Player_SetForcedTeamIndex: Invalid team index."); + } + else + { + player.team_forced = team_index; + break; + } + } } - if (MUTATOR_CALLHOOK(Player_ChangeTeamKill, player) == true) +} + +void Player_DetermineForcedTeam(entity player) +{ + if (autocvar_g_campaign) { - return; + if (IS_REAL_CLIENT(player)) // only players, not bots + { + if (Team_IsValidIndex(autocvar_g_campaign_forceteam)) + { + player.team_forced = autocvar_g_campaign_forceteam; + } + else + { + player.team_forced = TEAM_FORCE_DEFAULT; + } + } + } + else if (PlayerInList(player, autocvar_g_forced_team_red)) + { + player.team_forced = 1; + } + else if (PlayerInList(player, autocvar_g_forced_team_blue)) + { + player.team_forced = 2; + } + else if (PlayerInList(player, autocvar_g_forced_team_yellow)) + { + player.team_forced = 3; + } + else if (PlayerInList(player, autocvar_g_forced_team_pink)) + { + player.team_forced = 4; + } + else + { + switch (autocvar_g_forced_team_otherwise) + { + case "red": + { + player.team_forced = 1; + break; + } + case "blue": + { + player.team_forced = 2; + break; + } + case "yellow": + { + player.team_forced = 3; + break; + } + case "pink": + { + player.team_forced = 4; + break; + } + case "spectate": + case "spectator": + { + player.team_forced = TEAM_FORCE_SPECTATOR; + break; + } + default: + { + player.team_forced = TEAM_FORCE_DEFAULT; + break; + } + } + } + if (!teamplay && Player_HasRealForcedTeam(player)) + { + player.team_forced = TEAM_FORCE_DEFAULT; } - Damage(player, player, player, 100000, DEATH_TEAMCHANGE.m_id, DMG_NOWEP, - player.origin, '0 0 0'); } -void LogTeamchange(float player_id, float team_number, int type) +void TeamBalance_JoinBestTeam(entity player) { - if(!autocvar_sv_eventlog) + //PrintToChatAll(sprintf("TeamBalance_JoinBestTeam: %s", player.netname)); + if (!teamplay) + { return; - - if(player_id < 1) + } + if (player.bot_forced_team) + { return; - - GameLogEcho(strcat(":team:", ftos(player_id), ":", ftos(team_number), ":", ftos(type))); + } + entity balance = TeamBalance_CheckAllowedTeams(player); + if (Player_HasRealForcedTeam(player)) + { + int forced_team_index = player.team_forced; + bool is_team_allowed = TeamBalance_IsTeamAllowedInternal(balance, + forced_team_index); + TeamBalance_Destroy(balance); + if (!is_team_allowed) + { + return; + } + if (!SetPlayerTeam(player, forced_team_index, TEAM_CHANGE_AUTO)) + { + return; + } + return; + } + int best_team_index = TeamBalance_FindBestTeam(balance, player, true); + TeamBalance_Destroy(balance); + if (!SetPlayerTeam(player, best_team_index, TEAM_CHANGE_AUTO)) + { + return; + } } entity TeamBalance_CheckAllowedTeams(entity for_whom) @@ -383,7 +574,7 @@ entity TeamBalance_CheckAllowedTeams(entity for_whom) // if player has a forced team, ONLY allow that one for (int i = 1; i <= NUM_TEAMS; ++i) { - if (for_whom.team_forced == Team_IndexToTeam(i) && + if (for_whom.team_forced == i && TeamBalance_IsTeamAllowedInternal(balance, i)) { TeamBalance_BanTeamsExcept(balance, i); @@ -483,13 +674,15 @@ void TeamBalance_GetTeamCounts(entity balance, entity ignore) continue; } int team_num; - if (IS_PLAYER(it) || it.caplayer) + // TODO: Reconsider when the player is truly on the team. + if (IS_CLIENT(it) || (it.caplayer)) { team_num = it.team; } - else if (it.team_forced > 0) + else if (Player_HasRealForcedTeam(it)) { - team_num = it.team_forced; // reserve the spot + // Do we really need this? Probably not. + team_num = Team_IndexToTeam(it.team_forced); // reserve the spot } else { @@ -631,52 +824,6 @@ int TeamBalance_FindBestTeams(entity balance, entity player, bool use_score) return team_bits; } -void TeamBalance_JoinBestTeam(entity this) -{ - //PrintToChatAll(sprintf("JoinBestTeam: %s", this.netname)); - if (!teamplay) - { - return; - } - if (this.bot_forced_team) - { - return; - } - int old_team_index = Team_TeamToIndex(this.team); - entity balance = TeamBalance_CheckAllowedTeams(this); - if (this.team_forced > 0) - { - int forced_team_index = Team_TeamToIndex(this.team_forced); - bool is_team_allowed = TeamBalance_IsTeamAllowedInternal(balance, - forced_team_index); - TeamBalance_Destroy(balance); - if (!is_team_allowed) - { - return; - } - if (!SetPlayerTeam(this, forced_team_index, TEAM_CHANGE_AUTO)) - { - return; - } - if ((old_team_index != -1) && !IS_BOT_CLIENT(this)) - { - TeamBalance_AutoBalanceBots(forced_team_index, old_team_index); - } - return; - } - int best_team_index = TeamBalance_FindBestTeam(balance, this, true); - TeamBalance_Destroy(balance); - PlayerScore_Clear(this); - if (!SetPlayerTeam(this, best_team_index, TEAM_CHANGE_AUTO)) - { - return; - } - if ((old_team_index != -1) && !IS_BOT_CLIENT(this)) - { - TeamBalance_AutoBalanceBots(best_team_index, old_team_index); - } -} - int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b, entity player, bool use_score) { @@ -708,74 +855,172 @@ int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b, return TeamBalance_CompareTeamsInternal(team_a, team_b, player, use_score); } -void TeamBalance_AutoBalanceBots(int source_team_index, - int destination_team_index) +void TeamBalance_AutoBalanceBots() { - if (!Team_IsValidIndex(source_team_index)) - { - LOG_WARNF("TeamBalance_AutoBalanceBots: " - "Source team index is invalid: %f", source_team_index); - return; - } - if (!Team_IsValidIndex(destination_team_index)) - { - LOG_WARNF("TeamBalance_AutoBalanceBots: " - "Destination team index is invalid: %f", destination_team_index); - return; - } if (!autocvar_g_balance_teams || !autocvar_g_balance_teams_prevent_imbalance) { return; } + //PrintToChatAll("TeamBalance_AutoBalanceBots"); entity balance = TeamBalance_CheckAllowedTeams(NULL); TeamBalance_GetTeamCounts(balance, NULL); - entity source_team = TeamBalance_GetTeamFromIndex(balance, - source_team_index); - entity destination_team = TeamBalance_GetTeamFromIndex(balance, - destination_team_index); - if ((source_team.m_num_bots == 0) || (source_team.m_num_players <= - destination_team.m_num_players)) + int smallest_team_index = 0; + int smallest_team_player_count = 0; + for (int i = 1; i <= NUM_TEAMS; ++i) { - TeamBalance_Destroy(balance); - return; + entity team_ = TeamBalance_GetTeamFromIndex(balance, i); + if (!TeamBalanceTeam_IsAllowed(team_)) + { + continue; + } + int player_count = TeamBalanceTeam_GetNumberOfPlayers(team_); + if (smallest_team_index == 0) + { + smallest_team_index = i; + smallest_team_player_count = player_count; + } + else if (player_count < smallest_team_player_count) + { + smallest_team_index = i; + smallest_team_player_count = player_count; + } + } + //PrintToChatAll(sprintf("Smallest team: %f", smallest_team_index)); + //PrintToChatAll(sprintf("Smallest team players: %f", smallest_team_player_count)); + entity switchable_bot = NULL; + int teams = BITS(NUM_TEAMS); + while (teams != 0) + { + int largest_team_index = TeamBalance_GetLargestTeamIndex(balance, + teams); + if (smallest_team_index == largest_team_index) + { + TeamBalance_Destroy(balance); + return; + } + entity largest_team = TeamBalance_GetTeamFromIndex(balance, + largest_team_index); + int largest_team_player_count = TeamBalanceTeam_GetNumberOfPlayers( + largest_team); + if (largest_team_player_count - smallest_team_player_count < 2) + { + TeamBalance_Destroy(balance); + return; + } + //PrintToChatAll(sprintf("Largest team: %f", largest_team_index)); + //PrintToChatAll(sprintf("Largest team players: %f", largest_team_player_count)); + switchable_bot = TeamBalance_GetPlayerForTeamSwitch(largest_team_index, + smallest_team_index, true); + if (switchable_bot != NULL) + { + break; + } + teams &= ~Team_IndexToBit(largest_team_index); } TeamBalance_Destroy(balance); - entity lowest_bot = NULL; + if (switchable_bot == NULL) + { + //PrintToChatAll("No bot found after searching through all the teams"); + return; + } + SetPlayerTeam(switchable_bot, smallest_team_index, TEAM_CHANGE_AUTO); +} + +int TeamBalance_GetLargestTeamIndex(entity balance, int teams) +{ + int largest_team_index = 0; + int largest_team_player_count = 0; + for (int i = 1; i <= NUM_TEAMS; ++i) + { + if (!(Team_IndexToBit(i) & teams)) + { + continue; + } + entity team_ = TeamBalance_GetTeamFromIndex(balance, i); + if (!TeamBalanceTeam_IsAllowed(team_)) + { + continue; + } + int player_count = TeamBalanceTeam_GetNumberOfPlayers(team_); + if (largest_team_index == 0) + { + largest_team_index = i; + largest_team_player_count = player_count; + } + else if (player_count > largest_team_player_count) + { + largest_team_index = i; + largest_team_player_count = player_count; + } + } + return largest_team_index; +} + +entity TeamBalance_GetPlayerForTeamSwitch(int source_team_index, + int destination_team_index, bool is_bot) +{ if (MUTATOR_CALLHOOK(TeamBalance_GetPlayerForTeamSwitch, source_team_index, - destination_team_index, true)) + destination_team_index, is_bot)) { - lowest_bot = M_ARGV(3, entity); + return M_ARGV(3, entity); } - else + entity lowest_player = NULL; + float lowest_score = FLOAT_MAX; + FOREACH_CLIENT(Entity_GetTeamIndex(it) == source_team_index, { - float lowest_score = FLOAT_MAX; - FOREACH_CLIENT(IS_BOT_CLIENT(it) && (Entity_GetTeamIndex(it) == - source_team_index), + if (IS_BOT_CLIENT(it) != is_bot) { - float temp_score = PlayerScore_Get(it, SP_SCORE); - if (temp_score >= lowest_score) - { - continue; - } - balance = TeamBalance_CheckAllowedTeams(it); - if (TeamBalance_IsTeamAllowed(balance, destination_team_index)) - { - lowest_bot = it; - lowest_score = temp_score; - } - TeamBalance_Destroy(balance); - }); + continue; + } + float temp_score = PlayerScore_Get(it, SP_SCORE); + if (temp_score >= lowest_score) + { + continue; + } + //PrintToChatAll(sprintf( + // "Found %s with lowest score, checking allowed teams", it.netname)); + entity balance = TeamBalance_CheckAllowedTeams(it); + if (TeamBalance_IsTeamAllowed(balance, source_team_index)) + { + //PrintToChatAll("Allowed"); + lowest_player = it; + lowest_score = temp_score; + } + else + { + //PrintToChatAll("Not allowed"); + } + TeamBalance_Destroy(balance); + }); + return lowest_player; +} + +void LogTeamChange(float player_id, float team_number, int type) +{ + if (!autocvar_sv_eventlog) + { + return; + } + if (player_id < 1) + { + return; } - if (lowest_bot == NULL) + GameLogEcho(sprintf(":team:%f:%f:%f", player_id, team_number, type)); +} + +void KillPlayerForTeamChange(entity player) +{ + if (IS_DEAD(player)) { return; } - if (!Player_SetTeamIndex(lowest_bot, destination_team_index)) + if (MUTATOR_CALLHOOK(Player_ChangeTeamKill, player) == true) { return; } - KillPlayerForTeamChange(lowest_bot); + Damage(player, player, player, 100000, DEATH_TEAMCHANGE.m_id, DMG_NOWEP, + player.origin, '0 0 0'); } bool TeamBalance_IsTeamAllowedInternal(entity balance, int index) @@ -866,101 +1111,24 @@ int TeamBalance_CompareTeamsInternal(entity team_a, entity team_b, return TEAMS_COMPARE_EQUAL; } -// Called when the player connects or when they change their color with "color" -// command. -void SV_ChangeTeam(entity this, float _color) +void SV_ChangeTeam(entity player, int new_color) { - //PrintToChatAll(sprintf("SV_ChangeTeam: %s, %f", this.netname, _color)); - - // in normal deathmatch we can just apply the color and we're done - if(!teamplay) - SetPlayerColors(this, _color); - - if(!IS_CLIENT(this)) - { - // since this is an engine function, and gamecode doesn't have any calls earlier than this, do the connecting message here - Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CONNECTING, this.netname); - return; - } - - if(!teamplay) - return; - - int source_color, destination_color; - int source_team_index, destination_team_index; - - source_color = this.clientcolors & 0x0F; - destination_color = _color & 0x0F; - - source_team_index = Team_TeamToIndex(source_color + 1); - destination_team_index = Team_TeamToIndex(destination_color + 1); - - if (destination_team_index == -1) - { - return; - } - - entity balance = TeamBalance_CheckAllowedTeams(this); - - if (destination_team_index == 1 && !TeamBalance_IsTeamAllowedInternal( - balance, 1)) - { - destination_team_index = 4; - } - if (destination_team_index == 4 && !TeamBalance_IsTeamAllowedInternal( - balance, 4)) - { - destination_team_index = 3; - } - if (destination_team_index == 3 && !TeamBalance_IsTeamAllowedInternal( - balance, 3)) - { - destination_team_index = 2; - } - if (destination_team_index == 2 && !TeamBalance_IsTeamAllowedInternal( - balance, 2)) - { - destination_team_index = 1; - } - - // not changing teams - if (source_color == destination_color) - { - SetPlayerTeam(this, destination_team_index, TEAM_CHANGE_MANUAL); - TeamBalance_Destroy(balance); - return; - } - - if((autocvar_g_campaign) || (autocvar_g_changeteam_banned && CS(this).wasplayer)) { - Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_NOTALLOWED); - return; // changing teams is not allowed - } - - // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless - if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance) - { - TeamBalance_GetTeamCounts(balance, this); - if ((Team_IndexToBit(destination_team_index) & - TeamBalance_FindBestTeams(balance, this, false)) == 0) - { - Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM); - TeamBalance_Destroy(balance); - return; - } - } - TeamBalance_Destroy(balance); - if (IS_PLAYER(this) && source_team_index != destination_team_index) + if (!teamplay) { - // reduce frags during a team change - PlayerScore_Clear(this); + SetPlayerColors(player, new_color); } - if (!SetPlayerTeam(this, destination_team_index, TEAM_CHANGE_MANUAL)) + // TODO: Should we really bother with this? + if(!IS_CLIENT(player)) { + // since this is an engine function, and gamecode doesn't have any calls earlier than this, do the connecting message here + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CONNECTING, + player.netname); return; } - if (source_team_index == -1) + if (!teamplay) { return; } - TeamBalance_AutoBalanceBots(destination_team_index, source_team_index); + Player_SetTeamIndexChecked(player, Team_TeamToIndex((new_color & 0x0F) + + 1)); } diff --git a/qcsrc/server/teamplay.qh b/qcsrc/server/teamplay.qh index 776a80d32..33f9d02d7 100644 --- a/qcsrc/server/teamplay.qh +++ b/qcsrc/server/teamplay.qh @@ -1,5 +1,13 @@ #pragma once +int autocvar_teamplay_mode; + +bool autocvar_g_changeteam_banned; +bool autocvar_teamplay_lockonrestart; + +bool autocvar_g_balance_teams; +bool autocvar_g_balance_teams_prevent_imbalance; + bool lockteams; // ========================== Global teams API ================================ @@ -83,13 +91,25 @@ void SetPlayerColors(entity player, float _color); /// \return True if team switch was successful, false otherwise. bool Player_SetTeamIndex(entity player, int index); +enum +{ + TEAM_CHANGE_AUTO = 2, ///< The team was selected by autobalance. + TEAM_CHANGE_MANUAL = 3, ///< Player has manually selected their team. + TEAM_CHANGE_SPECTATOR = 4 ///< Player is joining spectators. //TODO: Remove? +}; + /// \brief Sets the team of the player. /// \param[in,out] player Player to adjust. /// \param[in] team_index Index of the team to set. -/// \param[in] type ??? +/// \param[in] type Type of the team change. See TEAM_CHANGE constants. /// \return True if team switch was successful, false otherwise. bool SetPlayerTeam(entity player, int team_index, int type); +/// \brief Sets the team of the player with all sanity checks. +/// \param[in,out] player Player to adjust. +/// \param[in] team_index Index of the team to set. +void Player_SetTeamIndexChecked(entity player, int team_index); + /// \brief Moves player to the specified team. /// \param[in,out] client Client to move. /// \param[in] team_index Index of the team. @@ -97,21 +117,39 @@ bool SetPlayerTeam(entity player, int team_index, int type); /// \return True on success, false otherwise. bool MoveToTeam(entity client, int team_index, int type); -/// \brief Kills player as a result of team change. -/// \param[in,out] player Player to kill. -void KillPlayerForTeamChange(entity player); - enum { - TEAM_CHANGE_AUTO = 2, - TEAM_CHANGE_MANUAL = 3, - TEAM_CHANGE_SPECTATOR = 4 + TEAM_FORCE_SPECTATOR = -1, ///< Force the player to spectator team. + TEAM_FORCE_DEFAULT = 0 ///< Don't force any team. }; -void LogTeamchange(float player_id, float team_number, int type); +/// \brief Returns whether player has real forced team. Spectator team is +/// ignored. +/// \param[in] player Player to check. +/// \return True if player has real forced team, false otherwise. +bool Player_HasRealForcedTeam(entity player); + +/// \brief Returns the index of the forced team of the given player. +/// \param[in] player Player to check. +/// \return Index of the forced team. +int Player_GetForcedTeamIndex(entity player); + +/// \brief Sets the index of the forced team of the given player. +/// \param[in,out] player Player to adjust. +/// \param[in] team_index Index of the team to set. +void Player_SetForcedTeamIndex(entity player, int team_index); + +/// \brief Determines the forced team of the player using current global config. +/// \param[in,out] player Player to adjust. +void Player_DetermineForcedTeam(entity player); // ========================= Team balance API ================================= +/// \brief Assigns the given player to a team that will make the game most +/// balanced. +/// \param[in,out] player Player to assign. +void TeamBalance_JoinBestTeam(entity player); + /// \brief Checks whether the player can join teams according to global /// configuration and mutator settings. /// \param[in] for_whom Player to check for. Pass NULL for global rules. @@ -174,8 +212,6 @@ int TeamBalance_FindBestTeam(entity balance, entity player, bool ignore_player); /// function. int TeamBalance_FindBestTeams(entity balance, entity player, bool use_score); -void TeamBalance_JoinBestTeam(entity this); - /// \brief Describes the result of comparing teams. enum { @@ -198,13 +234,33 @@ int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b, entity player, bool use_score); /// \brief Switches a bot from one team to another if teams are not balanced. -/// \param[in] source_team_index Index of the team to switch from. +void TeamBalance_AutoBalanceBots(); + +/// \brief Returns the index of the team with most players that is contained in +/// the given bitmask of teams. +/// \param[in] balance Team balance entity. +/// \param[in] teams Bitmask of teams to search in. +/// \return Index of the team with most players. +int TeamBalance_GetLargestTeamIndex(entity balance, int teams); + +/// \brief Returns the player who is the most suitable for switching between +/// the given teams. +/// \param[in] source_team_index Index of the team to search in. /// \param[in] destination_team_index Index of the team to switch to. -void TeamBalance_AutoBalanceBots(int source_team_index, - int destination_team_index); +/// \param[in] is_bot True to search for bot, false for human. +/// \return Player who is the most suitable for switching between the given +/// teams or NULL if not found. +entity TeamBalance_GetPlayerForTeamSwitch(int source_team_index, + int destination_team_index, bool is_bot); // ============================ Internal API ================================== +void LogTeamChange(float player_id, float team_number, int type); + +/// \brief Kills player as a result of team change. +/// \param[in,out] player Player to kill. +void KillPlayerForTeamChange(entity player); + /// \brief Returns whether the team change to the specified team is allowed. /// \param[in] balance Team balance entity. /// \param[in] index Index of the team. @@ -262,3 +318,9 @@ int TeamBalanceTeam_GetNumberOfBots(entity team_ent); /// function. int TeamBalance_CompareTeamsInternal(entity team_a, entity team_index_b, entity player, bool use_score); + +/// \brief Called when the player connects or when they change their color with +/// the "color" command. +/// \param[in,out] player Player that requested a new color. +/// \param[in] new_color Requested color. +void SV_ChangeTeam(entity player, int new_color); diff --git a/xonotic-server.cfg b/xonotic-server.cfg index 4898cb6af..efba68a90 100644 --- a/xonotic-server.cfg +++ b/xonotic-server.cfg @@ -258,9 +258,7 @@ set g_teamdamage_resetspeed 20 "for teamplay_mode 4: how fast player's team set g_balance_teams 1 "automatically balance out players entering instead of asking them for their preferred team" set g_balance_teams_prevent_imbalance 1 "prevent players from changing to larger teams" -set g_balance_teams_scorefactor 0.25 "at the end of the game, take score into account instead of team size by this amount (beware: values over 0.5 mean that a x:0 score imbalance will cause ALL new players to prefer the losing team at the end, despite numbers)" set g_changeteam_banned 0 "not allowed to change team" -set g_changeteam_fragtransfer 0 "% of frags you get to keep when you change teams (rounded down)" set sv_teamnagger 1 "enable a nag message when the teams are unbalanced"