// Copyright (c) ZeniMax Media Inc. // Licensed under the GNU General Public License 2.0. #include "../g_local.h" #include "../m_player.h" #include enum match_t { MATCH_NONE, MATCH_SETUP, MATCH_PREGAME, MATCH_GAME, MATCH_POST }; enum elect_t { ELECT_NONE, ELECT_MATCH, ELECT_ADMIN, ELECT_MAP }; struct ctfgame_t { int team1, team2; int total1, total2; // these are only set when going into intermission except in teamplay gtime_t last_flag_capture; int last_capture_team; match_t match; // match state gtime_t matchtime; // time for match start/end (depends on state) int lasttime; // last time update, explicitly truncated to seconds bool countdown; // has audio countdown started? elect_t election; // election type edict_t *etarget; // for admin election, who's being elected char elevel[32]; // for map election, target level int evotes; // votes so far int needvotes; // votes needed gtime_t electtime; // remaining time until election times out char emsg[256]; // election name int warnactive; // true if stat string 30 is active ghost_t ghosts[MAX_CLIENTS]; // ghost codes }; ctfgame_t ctfgame; cvar_t *ctf; cvar_t *teamplay; cvar_t *g_teamplay_force_join; // [Paril-KEX] bool G_TeamplayEnabled() { return ctf->integer || teamplay->integer; } // [Paril-KEX] void G_AdjustTeamScore(ctfteam_t team, int32_t offset) { if (team == CTF_TEAM1) ctfgame.total1 += offset; else if (team == CTF_TEAM2) ctfgame.total2 += offset; } cvar_t *competition; cvar_t *matchlock; cvar_t *electpercentage; cvar_t *matchtime; cvar_t *matchsetuptime; cvar_t *matchstarttime; cvar_t *admin_password; cvar_t *allow_admin; cvar_t *warp_list; cvar_t *warn_unbalanced; // Index for various CTF pics, this saves us from calling gi.imageindex // all the time and saves a few CPU cycles since we don't have to do // a bunch of string compares all the time. // These are set in CTFPrecache() called from worldspawn int imageindex_i_ctf1; int imageindex_i_ctf2; int imageindex_i_ctf1d; int imageindex_i_ctf2d; int imageindex_i_ctf1t; int imageindex_i_ctf2t; int imageindex_i_ctfj; int imageindex_sbfctf1; int imageindex_sbfctf2; int imageindex_ctfsb1; int imageindex_ctfsb2; int modelindex_flag1, modelindex_flag2; // [Paril-KEX] constexpr item_id_t tech_ids[] = { IT_TECH_RESISTANCE, IT_TECH_STRENGTH, IT_TECH_HASTE, IT_TECH_REGENERATION }; /*--------------------------------------------------------------------------*/ #ifndef KEX_Q2_GAME /* ================= findradius Returns entities that have origins within a spherical area findradius (origin, radius) ================= */ static edict_t *loc_findradius(edict_t *from, const vec3_t &org, float rad) { vec3_t eorg; int j; if (!from) from = g_edicts; else from++; for (; from < &g_edicts[globals.num_edicts]; from++) { if (!from->inuse) continue; for (j = 0; j < 3; j++) eorg[j] = org[j] - (from->s.origin[j] + (from->mins[j] + from->maxs[j]) * 0.5f); if (eorg.length() > rad) continue; return from; } return nullptr; } #endif static void loc_buildboxpoints(vec3_t (&p)[8], const vec3_t &org, const vec3_t &mins, const vec3_t &maxs) { p[0] = org + mins; p[1] = p[0]; p[1][0] -= mins[0]; p[2] = p[0]; p[2][1] -= mins[1]; p[3] = p[0]; p[3][0] -= mins[0]; p[3][1] -= mins[1]; p[4] = org + maxs; p[5] = p[4]; p[5][0] -= maxs[0]; p[6] = p[0]; p[6][1] -= maxs[1]; p[7] = p[0]; p[7][0] -= maxs[0]; p[7][1] -= maxs[1]; } static bool loc_CanSee(edict_t *targ, edict_t *inflictor) { trace_t trace; vec3_t targpoints[8]; int i; vec3_t viewpoint; // bmodels need special checking because their origin is 0,0,0 if (targ->movetype == MOVETYPE_PUSH) return false; // bmodels not supported loc_buildboxpoints(targpoints, targ->s.origin, targ->mins, targ->maxs); viewpoint = inflictor->s.origin; viewpoint[2] += inflictor->viewheight; for (i = 0; i < 8; i++) { trace = gi.traceline(viewpoint, targpoints[i], inflictor, MASK_SOLID); if (trace.fraction == 1.0f) return true; } return false; } # /*--------------------------------------------------------------------------*/ void CTFSpawn() { memset(&ctfgame, 0, sizeof(ctfgame)); CTFSetupTechSpawn(); if (competition->integer > 1) { ctfgame.match = MATCH_SETUP; ctfgame.matchtime = level.time + gtime_t::from_min(matchsetuptime->value); } } void CTFInit() { ctf = gi.cvar("ctf", "0", CVAR_SERVERINFO | CVAR_LATCH); competition = gi.cvar("competition", "0", CVAR_SERVERINFO); matchlock = gi.cvar("matchlock", "1", CVAR_SERVERINFO); electpercentage = gi.cvar("electpercentage", "66", CVAR_NOFLAGS); matchtime = gi.cvar("matchtime", "20", CVAR_SERVERINFO); matchsetuptime = gi.cvar("matchsetuptime", "10", CVAR_NOFLAGS); matchstarttime = gi.cvar("matchstarttime", "20", CVAR_NOFLAGS); admin_password = gi.cvar("admin_password", "", CVAR_NOFLAGS); allow_admin = gi.cvar("allow_admin", "1", CVAR_NOFLAGS); warp_list = gi.cvar("warp_list", "q2ctf1 q2ctf2 q2ctf3 q2ctf4 q2ctf5", CVAR_NOFLAGS); warn_unbalanced = gi.cvar("warn_unbalanced", "0", CVAR_NOFLAGS); } /* * Precache CTF items */ void CTFPrecache() { imageindex_i_ctf1 = gi.imageindex("i_ctf1"); imageindex_i_ctf2 = gi.imageindex("i_ctf2"); imageindex_i_ctf1d = gi.imageindex("i_ctf1d"); imageindex_i_ctf2d = gi.imageindex("i_ctf2d"); imageindex_i_ctf1t = gi.imageindex("i_ctf1t"); imageindex_i_ctf2t = gi.imageindex("i_ctf2t"); imageindex_i_ctfj = gi.imageindex("i_ctfj"); imageindex_sbfctf1 = gi.imageindex("sbfctf1"); imageindex_sbfctf2 = gi.imageindex("sbfctf2"); imageindex_ctfsb1 = gi.imageindex("tag4"); imageindex_ctfsb2 = gi.imageindex("tag5"); modelindex_flag1 = gi.modelindex("players/male/flag1.md2"); modelindex_flag2 = gi.modelindex("players/male/flag2.md2"); PrecacheItem(GetItemByIndex(IT_WEAPON_GRAPPLE)); } /*--------------------------------------------------------------------------*/ const char *CTFTeamName(int team) { switch (team) { case CTF_TEAM1: return "RED"; case CTF_TEAM2: return "BLUE"; case CTF_NOTEAM: return "SPECTATOR"; } return "UNKNOWN"; // Hanzo pointed out this was spelled wrong as "UKNOWN" } const char *CTFOtherTeamName(int team) { switch (team) { case CTF_TEAM1: return "BLUE"; case CTF_TEAM2: return "RED"; } return "UNKNOWN"; // Hanzo pointed out this was spelled wrong as "UKNOWN" } int CTFOtherTeam(int team) { switch (team) { case CTF_TEAM1: return CTF_TEAM2; case CTF_TEAM2: return CTF_TEAM1; } return -1; // invalid value } /*--------------------------------------------------------------------------*/ float PlayersRangeFromSpot(edict_t *spot); bool SpawnPointClear(edict_t *spot); void CTFAssignSkin(edict_t *ent, const char *s) { int playernum = ent - g_edicts - 1; std::string_view t(s); if (size_t i = t.find_first_of('/'); i != std::string_view::npos) t = t.substr(0, i + 1); else t = "male/"; switch (ent->client->resp.ctf_team) { case CTF_TEAM1: t = G_Fmt("{}\\{}{}\\default", ent->client->pers.netname, t, CTF_TEAM1_SKIN); break; case CTF_TEAM2: t = G_Fmt("{}\\{}{}\\default", ent->client->pers.netname, t, CTF_TEAM2_SKIN); break; default: t = G_Fmt("{}\\{}\\default", ent->client->pers.netname, s); break; } gi.configstring(CS_PLAYERSKINS + playernum, t.data()); // gi.LocClient_Print(ent, PRINT_HIGH, "$g_assigned_team", ent->client->pers.netname); } void CTFAssignTeam(gclient_t *who) { edict_t *player; uint32_t team1count = 0, team2count = 0; who->resp.ctf_state = 0; if (!g_teamplay_force_join->integer && !(g_edicts[1 + (who - game.clients)].svflags & SVF_BOT)) { who->resp.ctf_team = CTF_NOTEAM; return; } for (uint32_t i = 1; i <= game.maxclients; i++) { player = &g_edicts[i]; if (!player->inuse || player->client == who) continue; switch (player->client->resp.ctf_team) { case CTF_TEAM1: team1count++; break; case CTF_TEAM2: team2count++; break; default: break; } } if (team1count < team2count) who->resp.ctf_team = CTF_TEAM1; else if (team2count < team1count) who->resp.ctf_team = CTF_TEAM2; else if (brandom()) who->resp.ctf_team = CTF_TEAM1; else who->resp.ctf_team = CTF_TEAM2; } /* ================ SelectCTFSpawnPoint go to a ctf point, but NOT the two points closest to other players ================ */ edict_t *SelectCTFSpawnPoint(edict_t *ent, bool force_spawn) { if (ent->client->resp.ctf_state) { select_spawn_result_t result = SelectDeathmatchSpawnPoint(g_dm_spawn_farthest->integer, force_spawn, false); if (result.any_valid) return result.spot; } const char *cname; switch (ent->client->resp.ctf_team) { case CTF_TEAM1: cname = "info_player_team1"; break; case CTF_TEAM2: cname = "info_player_team2"; break; default: { select_spawn_result_t result = SelectDeathmatchSpawnPoint(g_dm_spawn_farthest->integer, force_spawn, true); if (result.any_valid) return result.spot; gi.Com_Error("can't find suitable spectator spawn point"); return nullptr; } } static std::vector spawn_points; edict_t *spot = nullptr; spawn_points.clear(); while ((spot = G_FindByString<&edict_t::classname>(spot, cname)) != nullptr) spawn_points.push_back(spot); if (!spawn_points.size()) { select_spawn_result_t result = SelectDeathmatchSpawnPoint(g_dm_spawn_farthest->integer, force_spawn, true); if (!result.any_valid) gi.Com_Error("can't find suitable CTF spawn point"); return result.spot; } std::shuffle(spawn_points.begin(), spawn_points.end(), mt_rand); for (auto &point : spawn_points) if (SpawnPointClear(point)) return point; if (force_spawn) return random_element(spawn_points); return nullptr; } /*------------------------------------------------------------------------*/ /* CTFFragBonuses Calculate the bonuses for flag defense, flag carrier defense, etc. Note that bonuses are not cumaltive. You get one, they are in importance order. */ void CTFFragBonuses(edict_t *targ, edict_t *inflictor, edict_t *attacker) { edict_t *ent; item_id_t flag_item, enemy_flag_item; int otherteam; edict_t *flag, *carrier = nullptr; const char *c; vec3_t v1, v2; if (targ->client && attacker->client) { if (attacker->client->resp.ghost) if (attacker != targ) attacker->client->resp.ghost->kills++; if (targ->client->resp.ghost) targ->client->resp.ghost->deaths++; } // no bonus for fragging yourself if (!targ->client || !attacker->client || targ == attacker) return; otherteam = CTFOtherTeam(targ->client->resp.ctf_team); if (otherteam < 0) return; // whoever died isn't on a team // same team, if the flag at base, check to he has the enemy flag if (targ->client->resp.ctf_team == CTF_TEAM1) { flag_item = IT_FLAG1; enemy_flag_item = IT_FLAG2; } else { flag_item = IT_FLAG2; enemy_flag_item = IT_FLAG1; } // did the attacker frag the flag carrier? if (targ->client->pers.inventory[enemy_flag_item]) { attacker->client->resp.ctf_lastfraggedcarrier = level.time; attacker->client->resp.score += CTF_FRAG_CARRIER_BONUS; gi.LocClient_Print(attacker, PRINT_MEDIUM, "$g_bonus_enemy_carrier", CTF_FRAG_CARRIER_BONUS); // the target had the flag, clear the hurt carrier // field on the other team for (uint32_t i = 1; i <= game.maxclients; i++) { ent = g_edicts + i; if (ent->inuse && ent->client->resp.ctf_team == otherteam) ent->client->resp.ctf_lasthurtcarrier = 0_ms; } return; } if (targ->client->resp.ctf_lasthurtcarrier && level.time - targ->client->resp.ctf_lasthurtcarrier < CTF_CARRIER_DANGER_PROTECT_TIMEOUT && !attacker->client->pers.inventory[flag_item]) { // attacker is on the same team as the flag carrier and // fragged a guy who hurt our flag carrier attacker->client->resp.score += CTF_CARRIER_DANGER_PROTECT_BONUS; gi.LocBroadcast_Print(PRINT_MEDIUM, "$g_bonus_flag_defense", attacker->client->pers.netname, CTFTeamName(attacker->client->resp.ctf_team)); if (attacker->client->resp.ghost) attacker->client->resp.ghost->carrierdef++; return; } // flag and flag carrier area defense bonuses // we have to find the flag and carrier entities // find the flag switch (attacker->client->resp.ctf_team) { case CTF_TEAM1: c = "item_flag_team1"; break; case CTF_TEAM2: c = "item_flag_team2"; break; default: return; } flag = nullptr; while ((flag = G_FindByString<&edict_t::classname>(flag, c)) != nullptr) { if (!(flag->spawnflags & SPAWNFLAG_ITEM_DROPPED)) break; } if (!flag) return; // can't find attacker's flag // find attacker's team's flag carrier for (uint32_t i = 1; i <= game.maxclients; i++) { carrier = g_edicts + i; if (carrier->inuse && carrier->client->pers.inventory[flag_item]) break; carrier = nullptr; } // ok we have the attackers flag and a pointer to the carrier // check to see if we are defending the base's flag v1 = targ->s.origin - flag->s.origin; v2 = attacker->s.origin - flag->s.origin; if ((v1.length() < CTF_TARGET_PROTECT_RADIUS || v2.length() < CTF_TARGET_PROTECT_RADIUS || loc_CanSee(flag, targ) || loc_CanSee(flag, attacker)) && attacker->client->resp.ctf_team != targ->client->resp.ctf_team) { // we defended the base flag attacker->client->resp.score += CTF_FLAG_DEFENSE_BONUS; if (flag->solid == SOLID_NOT) gi.LocBroadcast_Print(PRINT_MEDIUM, "$g_bonus_defend_base", attacker->client->pers.netname, CTFTeamName(attacker->client->resp.ctf_team)); else gi.LocBroadcast_Print(PRINT_MEDIUM, "$g_bonus_defend_flag", attacker->client->pers.netname, CTFTeamName(attacker->client->resp.ctf_team)); if (attacker->client->resp.ghost) attacker->client->resp.ghost->basedef++; return; } if (carrier && carrier != attacker) { v1 = targ->s.origin - carrier->s.origin; v2 = attacker->s.origin - carrier->s.origin; if (v1.length() < CTF_ATTACKER_PROTECT_RADIUS || v2.length() < CTF_ATTACKER_PROTECT_RADIUS || loc_CanSee(carrier, targ) || loc_CanSee(carrier, attacker)) { attacker->client->resp.score += CTF_CARRIER_PROTECT_BONUS; gi.LocBroadcast_Print(PRINT_MEDIUM, "$g_bonus_defend_carrier", attacker->client->pers.netname, CTFTeamName(attacker->client->resp.ctf_team)); if (attacker->client->resp.ghost) attacker->client->resp.ghost->carrierdef++; return; } } } void CTFCheckHurtCarrier(edict_t *targ, edict_t *attacker) { item_id_t flag_item; if (!targ->client || !attacker->client) return; if (targ->client->resp.ctf_team == CTF_TEAM1) flag_item = IT_FLAG2; else flag_item = IT_FLAG1; if (targ->client->pers.inventory[flag_item] && targ->client->resp.ctf_team != attacker->client->resp.ctf_team) attacker->client->resp.ctf_lasthurtcarrier = level.time; } /*------------------------------------------------------------------------*/ void CTFResetFlag(int ctf_team) { const char *c; edict_t *ent; switch (ctf_team) { case CTF_TEAM1: c = "item_flag_team1"; break; case CTF_TEAM2: c = "item_flag_team2"; break; default: return; } ent = nullptr; while ((ent = G_FindByString<&edict_t::classname>(ent, c)) != nullptr) { if (ent->spawnflags.has(SPAWNFLAG_ITEM_DROPPED)) G_FreeEdict(ent); else { ent->svflags &= ~SVF_NOCLIENT; ent->solid = SOLID_TRIGGER; gi.linkentity(ent); ent->s.event = EV_ITEM_RESPAWN; } } } void CTFResetFlags() { CTFResetFlag(CTF_TEAM1); CTFResetFlag(CTF_TEAM2); } bool CTFPickup_Flag(edict_t *ent, edict_t *other) { int ctf_team; edict_t *player; item_id_t flag_item, enemy_flag_item; // figure out what team this flag is if (ent->item->id == IT_FLAG1) ctf_team = CTF_TEAM1; else if (ent->item->id == IT_FLAG2) ctf_team = CTF_TEAM2; else { gi.LocClient_Print(ent, PRINT_HIGH, "Don't know what team the flag is on.\n"); return false; } // same team, if the flag at base, check to he has the enemy flag if (ctf_team == CTF_TEAM1) { flag_item = IT_FLAG1; enemy_flag_item = IT_FLAG2; } else { flag_item = IT_FLAG2; enemy_flag_item = IT_FLAG1; } if (ctf_team == other->client->resp.ctf_team) { if (!(ent->spawnflags & SPAWNFLAG_ITEM_DROPPED)) { // the flag is at home base. if the player has the enemy // flag, he's just won! if (other->client->pers.inventory[enemy_flag_item]) { gi.LocBroadcast_Print(PRINT_HIGH, "$g_flag_captured", other->client->pers.netname, CTFOtherTeamName(ctf_team)); other->client->pers.inventory[enemy_flag_item] = 0; ctfgame.last_flag_capture = level.time; ctfgame.last_capture_team = ctf_team; if (ctf_team == CTF_TEAM1) ctfgame.team1++; else ctfgame.team2++; gi.sound(ent, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); // other gets another 10 frag bonus other->client->resp.score += CTF_CAPTURE_BONUS; if (other->client->resp.ghost) other->client->resp.ghost->caps++; // Ok, let's do the player loop, hand out the bonuses for (uint32_t i = 1; i <= game.maxclients; i++) { player = &g_edicts[i]; if (!player->inuse) continue; if (player->client->resp.ctf_team != other->client->resp.ctf_team) player->client->resp.ctf_lasthurtcarrier = -5_sec; else if (player->client->resp.ctf_team == other->client->resp.ctf_team) { if (player != other) player->client->resp.score += CTF_TEAM_BONUS; // award extra points for capture assists if (player->client->resp.ctf_lastreturnedflag && player->client->resp.ctf_lastreturnedflag + CTF_RETURN_FLAG_ASSIST_TIMEOUT > level.time) { gi.LocBroadcast_Print(PRINT_HIGH, "$g_bonus_assist_return", player->client->pers.netname); player->client->resp.score += CTF_RETURN_FLAG_ASSIST_BONUS; } if (player->client->resp.ctf_lastfraggedcarrier && player->client->resp.ctf_lastfraggedcarrier + CTF_FRAG_CARRIER_ASSIST_TIMEOUT > level.time) { gi.LocBroadcast_Print(PRINT_HIGH, "$g_bonus_assist_frag_carrier", player->client->pers.netname); player->client->resp.score += CTF_FRAG_CARRIER_ASSIST_BONUS; } } } CTFResetFlags(); return false; } return false; // its at home base already } // hey, its not home. return it by teleporting it back gi.LocBroadcast_Print(PRINT_HIGH, "$g_returned_flag", other->client->pers.netname, CTFTeamName(ctf_team)); other->client->resp.score += CTF_RECOVERY_BONUS; other->client->resp.ctf_lastreturnedflag = level.time; gi.sound(ent, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex("ctf/flagret.wav"), 1, ATTN_NONE, 0); // CTFResetFlag will remove this entity! We must return false CTFResetFlag(ctf_team); return false; } // hey, its not our flag, pick it up gi.LocBroadcast_Print(PRINT_HIGH, "$g_got_flag", other->client->pers.netname, CTFTeamName(ctf_team)); other->client->resp.score += CTF_FLAG_BONUS; other->client->pers.inventory[flag_item] = 1; other->client->resp.ctf_flagsince = level.time; // pick up the flag // if it's not a dropped flag, we just make is disappear // if it's dropped, it will be removed by the pickup caller if (!(ent->spawnflags & SPAWNFLAG_ITEM_DROPPED)) { ent->flags |= FL_RESPAWN; ent->svflags |= SVF_NOCLIENT; ent->solid = SOLID_NOT; } return true; } TOUCH(CTFDropFlagTouch) (edict_t *ent, edict_t *other, const trace_t &tr, bool other_touching_self) -> void { // owner (who dropped us) can't touch for two secs if (other == ent->owner && ent->nextthink - level.time > CTF_AUTO_FLAG_RETURN_TIMEOUT - 2_sec) return; Touch_Item(ent, other, tr, other_touching_self); } THINK(CTFDropFlagThink) (edict_t *ent) -> void { // auto return the flag // reset flag will remove ourselves if (ent->item->id == IT_FLAG1) { CTFResetFlag(CTF_TEAM1); gi.LocBroadcast_Print(PRINT_HIGH, "$g_flag_returned", CTFTeamName(CTF_TEAM1)); } else if (ent->item->id == IT_FLAG2) { CTFResetFlag(CTF_TEAM2); gi.LocBroadcast_Print(PRINT_HIGH, "$g_flag_returned", CTFTeamName(CTF_TEAM2)); } } // Called from PlayerDie, to drop the flag from a dying player void CTFDeadDropFlag(edict_t *self) { edict_t *dropped = nullptr; if (self->client->pers.inventory[IT_FLAG1]) { dropped = Drop_Item(self, GetItemByIndex(IT_FLAG1)); self->client->pers.inventory[IT_FLAG1] = 0; gi.LocBroadcast_Print(PRINT_HIGH, "$g_lost_flag", self->client->pers.netname, CTFTeamName(CTF_TEAM1)); } else if (self->client->pers.inventory[IT_FLAG2]) { dropped = Drop_Item(self, GetItemByIndex(IT_FLAG2)); self->client->pers.inventory[IT_FLAG2] = 0; gi.LocBroadcast_Print(PRINT_HIGH, "$g_lost_flag", self->client->pers.netname, CTFTeamName(CTF_TEAM2)); } if (dropped) { dropped->think = CTFDropFlagThink; dropped->nextthink = level.time + CTF_AUTO_FLAG_RETURN_TIMEOUT; dropped->touch = CTFDropFlagTouch; } } void CTFDrop_Flag(edict_t *ent, gitem_t *item) { if (brandom()) gi.LocClient_Print(ent, PRINT_HIGH, "$g_lusers_drop_flags"); else gi.LocClient_Print(ent, PRINT_HIGH, "$g_winners_drop_flags"); } THINK(CTFFlagThink) (edict_t *ent) -> void { if (ent->solid != SOLID_NOT) ent->s.frame = 173 + (((ent->s.frame - 173) + 1) % 16); ent->nextthink = level.time + 10_hz; } THINK(CTFFlagSetup) (edict_t *ent) -> void { trace_t tr; vec3_t dest; ent->mins = { -15, -15, -15 }; ent->maxs = { 15, 15, 15 }; if (ent->model) gi.setmodel(ent, ent->model); else gi.setmodel(ent, ent->item->world_model); ent->solid = SOLID_TRIGGER; ent->movetype = MOVETYPE_TOSS; ent->touch = Touch_Item; ent->s.frame = 173; dest = ent->s.origin + vec3_t { 0, 0, -128 }; tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, dest, ent, MASK_SOLID); if (tr.startsolid) { gi.Com_PrintFmt("CTFFlagSetup: {} startsolid at {}\n", ent->classname, ent->s.origin); G_FreeEdict(ent); return; } ent->s.origin = tr.endpos; gi.linkentity(ent); ent->nextthink = level.time + 10_hz; ent->think = CTFFlagThink; } void CTFEffects(edict_t *player) { player->s.effects &= ~(EF_FLAG1 | EF_FLAG2); if (player->health > 0) { if (player->client->pers.inventory[IT_FLAG1]) { player->s.effects |= EF_FLAG1; } if (player->client->pers.inventory[IT_FLAG2]) { player->s.effects |= EF_FLAG2; } } if (player->client->pers.inventory[IT_FLAG1]) player->s.modelindex3 = modelindex_flag1; else if (player->client->pers.inventory[IT_FLAG2]) player->s.modelindex3 = modelindex_flag2; else player->s.modelindex3 = 0; } // called when we enter the intermission void CTFCalcScores() { ctfgame.total1 = ctfgame.total2 = 0; for (uint32_t i = 0; i < game.maxclients; i++) { if (!g_edicts[i + 1].inuse) continue; if (game.clients[i].resp.ctf_team == CTF_TEAM1) ctfgame.total1 += game.clients[i].resp.score; else if (game.clients[i].resp.ctf_team == CTF_TEAM2) ctfgame.total2 += game.clients[i].resp.score; } } // [Paril-KEX] end game rankings void CTFCalcRankings(std::array &player_ranks) { // we're all winners.. or losers. whatever if (ctfgame.total1 == ctfgame.total2) { player_ranks.fill(1); return; } ctfteam_t winning_team = (ctfgame.total1 > ctfgame.total2) ? CTF_TEAM1 : CTF_TEAM2; for (auto player : active_players()) if (player->client->pers.spawned && player->client->resp.ctf_team != CTF_NOTEAM) player_ranks[player->s.number - 1] = player->client->resp.ctf_team == winning_team ? 1 : 2; } void CheckEndTDMLevel() { if (ctfgame.total1 >= fraglimit->integer || ctfgame.total2 >= fraglimit->integer) { gi.LocBroadcast_Print(PRINT_HIGH, "$g_fraglimit_hit"); EndDMLevel(); } } void CTFID_f(edict_t *ent) { if (ent->client->resp.id_state) { gi.LocClient_Print(ent, PRINT_HIGH, "Disabling player identication display.\n"); ent->client->resp.id_state = false; } else { gi.LocClient_Print(ent, PRINT_HIGH, "Activating player identication display.\n"); ent->client->resp.id_state = true; } } static void CTFSetIDView(edict_t *ent) { vec3_t forward, dir; trace_t tr; edict_t *who, *best; float bd = 0, d; // only check every few frames if (level.time - ent->client->resp.lastidtime < 250_ms) return; ent->client->resp.lastidtime = level.time; ent->client->ps.stats[STAT_CTF_ID_VIEW] = 0; ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = 0; AngleVectors(ent->client->v_angle, forward, nullptr, nullptr); forward *= 1024; forward = ent->s.origin + forward; tr = gi.traceline(ent->s.origin, forward, ent, MASK_SOLID); if (tr.fraction < 1 && tr.ent && tr.ent->client) { ent->client->ps.stats[STAT_CTF_ID_VIEW] = (tr.ent - g_edicts); if (tr.ent->client->resp.ctf_team == CTF_TEAM1) ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf1; else if (tr.ent->client->resp.ctf_team == CTF_TEAM2) ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf2; return; } AngleVectors(ent->client->v_angle, forward, nullptr, nullptr); best = nullptr; for (uint32_t i = 1; i <= game.maxclients; i++) { who = g_edicts + i; if (!who->inuse || who->solid == SOLID_NOT) continue; dir = who->s.origin - ent->s.origin; dir.normalize(); d = forward.dot(dir); // we have teammate indicators that are better for this if (ent->client->resp.ctf_team == who->client->resp.ctf_team) continue; if (d > bd && loc_CanSee(ent, who)) { bd = d; best = who; } } if (bd > 0.90f) { ent->client->ps.stats[STAT_CTF_ID_VIEW] = (best - g_edicts); if (best->client->resp.ctf_team == CTF_TEAM1) ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf1; else if (best->client->resp.ctf_team == CTF_TEAM2) ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf2; } } void SetCTFStats(edict_t *ent) { uint32_t i; int p1, p2; edict_t *e; if (ctfgame.match > MATCH_NONE) ent->client->ps.stats[STAT_CTF_MATCH] = CONFIG_CTF_MATCH; else ent->client->ps.stats[STAT_CTF_MATCH] = 0; if (ctfgame.warnactive) ent->client->ps.stats[STAT_CTF_TEAMINFO] = CONFIG_CTF_TEAMINFO; else ent->client->ps.stats[STAT_CTF_TEAMINFO] = 0; // ghosting if (ent->client->resp.ghost) { ent->client->resp.ghost->score = ent->client->resp.score; Q_strlcpy(ent->client->resp.ghost->netname, ent->client->pers.netname, sizeof(ent->client->resp.ghost->netname)); ent->client->resp.ghost->number = ent->s.number; } // logo headers for the frag display ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = imageindex_ctfsb1; ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = imageindex_ctfsb2; bool blink = (level.time.milliseconds() % 1000) < 500; // if during intermission, we must blink the team header of the winning team if (level.intermissiontime && blink) { // blink half second // note that ctfgame.total[12] is set when we go to intermission if (ctfgame.team1 > ctfgame.team2) ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = 0; else if (ctfgame.team2 > ctfgame.team1) ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = 0; else if (ctfgame.total1 > ctfgame.total2) // frag tie breaker ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = 0; else if (ctfgame.total2 > ctfgame.total1) ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = 0; else { // tie game! ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = 0; ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = 0; } } // tech icon i = 0; ent->client->ps.stats[STAT_CTF_TECH] = 0; for (; i < q_countof(tech_ids); i++) { if (ent->client->pers.inventory[tech_ids[i]]) { ent->client->ps.stats[STAT_CTF_TECH] = gi.imageindex(GetItemByIndex(tech_ids[i])->icon); break; } } if (ctf->integer) { // figure out what icon to display for team logos // three states: // flag at base // flag taken // flag dropped p1 = imageindex_i_ctf1; e = G_FindByString<&edict_t::classname>(nullptr, "item_flag_team1"); if (e != nullptr) { if (e->solid == SOLID_NOT) { // not at base // check if on player p1 = imageindex_i_ctf1d; // default to dropped for (i = 1; i <= game.maxclients; i++) if (g_edicts[i].inuse && g_edicts[i].client->pers.inventory[IT_FLAG1]) { // enemy has it p1 = imageindex_i_ctf1t; break; } } else if (e->spawnflags.has(SPAWNFLAG_ITEM_DROPPED)) p1 = imageindex_i_ctf1d; // must be dropped } p2 = imageindex_i_ctf2; e = G_FindByString<&edict_t::classname>(nullptr, "item_flag_team2"); if (e != nullptr) { if (e->solid == SOLID_NOT) { // not at base // check if on player p2 = imageindex_i_ctf2d; // default to dropped for (i = 1; i <= game.maxclients; i++) if (g_edicts[i].inuse && g_edicts[i].client->pers.inventory[IT_FLAG2]) { // enemy has it p2 = imageindex_i_ctf2t; break; } } else if (e->spawnflags.has(SPAWNFLAG_ITEM_DROPPED)) p2 = imageindex_i_ctf2d; // must be dropped } ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = p1; ent->client->ps.stats[STAT_CTF_TEAM2_PIC] = p2; if (ctfgame.last_flag_capture && level.time - ctfgame.last_flag_capture < 5_sec) { if (ctfgame.last_capture_team == CTF_TEAM1) if (blink) ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = p1; else ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = 0; else if (blink) ent->client->ps.stats[STAT_CTF_TEAM2_PIC] = p2; else ent->client->ps.stats[STAT_CTF_TEAM2_PIC] = 0; } ent->client->ps.stats[STAT_CTF_TEAM1_CAPS] = ctfgame.team1; ent->client->ps.stats[STAT_CTF_TEAM2_CAPS] = ctfgame.team2; ent->client->ps.stats[STAT_CTF_FLAG_PIC] = 0; if (ent->client->resp.ctf_team == CTF_TEAM1 && ent->client->pers.inventory[IT_FLAG2] && (blink)) ent->client->ps.stats[STAT_CTF_FLAG_PIC] = imageindex_i_ctf2; else if (ent->client->resp.ctf_team == CTF_TEAM2 && ent->client->pers.inventory[IT_FLAG1] && (blink)) ent->client->ps.stats[STAT_CTF_FLAG_PIC] = imageindex_i_ctf1; } else { ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = imageindex_i_ctf1; ent->client->ps.stats[STAT_CTF_TEAM2_PIC] = imageindex_i_ctf2; ent->client->ps.stats[STAT_CTF_TEAM1_CAPS] = ctfgame.total1; ent->client->ps.stats[STAT_CTF_TEAM2_CAPS] = ctfgame.total2; } ent->client->ps.stats[STAT_CTF_JOINED_TEAM1_PIC] = 0; ent->client->ps.stats[STAT_CTF_JOINED_TEAM2_PIC] = 0; if (ent->client->resp.ctf_team == CTF_TEAM1) ent->client->ps.stats[STAT_CTF_JOINED_TEAM1_PIC] = imageindex_i_ctfj; else if (ent->client->resp.ctf_team == CTF_TEAM2) ent->client->ps.stats[STAT_CTF_JOINED_TEAM2_PIC] = imageindex_i_ctfj; if (ent->client->resp.id_state) CTFSetIDView(ent); else { ent->client->ps.stats[STAT_CTF_ID_VIEW] = 0; ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = 0; } } /*------------------------------------------------------------------------*/ /*QUAKED info_player_team1 (1 0 0) (-16 -16 -24) (16 16 32) potential team1 spawning position for ctf games */ void SP_info_player_team1(edict_t *self) { } /*QUAKED info_player_team2 (0 0 1) (-16 -16 -24) (16 16 32) potential team2 spawning position for ctf games */ void SP_info_player_team2(edict_t *self) { } /*------------------------------------------------------------------------*/ /* GRAPPLE */ /*------------------------------------------------------------------------*/ // ent is player void CTFPlayerResetGrapple(edict_t *ent) { if (ent->client && ent->client->ctf_grapple) CTFResetGrapple(ent->client->ctf_grapple); } // self is grapple, not player void CTFResetGrapple(edict_t *self) { if (!self->owner->client->ctf_grapple) return; gi.sound(self->owner, CHAN_WEAPON, gi.soundindex("weapons/grapple/grreset.wav"), self->owner->client->silencer_shots ? 0.2f : 1.0f, ATTN_NORM, 0); gclient_t *cl; cl = self->owner->client; cl->ctf_grapple = nullptr; cl->ctf_grapplereleasetime = level.time + 1_sec; cl->ctf_grapplestate = CTF_GRAPPLE_STATE_FLY; // we're firing, not on hook self->owner->flags &= ~FL_NO_KNOCKBACK; G_FreeEdict(self); } TOUCH(CTFGrappleTouch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void { float volume = 1.0; if (other == self->owner) return; if (self->owner->client->ctf_grapplestate != CTF_GRAPPLE_STATE_FLY) return; if (tr.surface && (tr.surface->flags & SURF_SKY)) { CTFResetGrapple(self); return; } self->velocity = {}; PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT); if (other->takedamage) { if (self->dmg) T_Damage(other, self, self->owner, self->velocity, self->s.origin, tr.plane.normal, self->dmg, 1, DAMAGE_NONE, MOD_GRAPPLE); CTFResetGrapple(self); return; } self->owner->client->ctf_grapplestate = CTF_GRAPPLE_STATE_PULL; // we're on hook self->enemy = other; self->solid = SOLID_NOT; if (self->owner->client->silencer_shots) volume = 0.2f; gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/grapple/grhit.wav"), volume, ATTN_NORM, 0); self->s.sound = gi.soundindex("weapons/grapple/grpull.wav"); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_SPARKS); gi.WritePosition(self->s.origin); gi.WriteDir(tr.plane.normal); gi.multicast(self->s.origin, MULTICAST_PVS, false); } // draw beam between grapple and self void CTFGrappleDrawCable(edict_t *self) { if (self->owner->client->ctf_grapplestate == CTF_GRAPPLE_STATE_HANG) return; vec3_t start, dir; P_ProjectSource(self->owner, self->owner->client->v_angle, { 7, 2, -9 }, start, dir); gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_GRAPPLE_CABLE_2); gi.WriteEntity(self->owner); gi.WritePosition(start); gi.WritePosition(self->s.origin); gi.multicast(self->s.origin, MULTICAST_PVS, false); } void SV_AddGravity(edict_t *ent); // pull the player toward the grapple void CTFGrapplePull(edict_t *self) { vec3_t hookdir, v; float vlen; if (self->owner->client->pers.weapon && self->owner->client->pers.weapon->id == IT_WEAPON_GRAPPLE && !(self->owner->client->newweapon || ((self->owner->client->latched_buttons | self->owner->client->buttons) & BUTTON_HOLSTER)) && self->owner->client->weaponstate != WEAPON_FIRING && self->owner->client->weaponstate != WEAPON_ACTIVATING) { if (!self->owner->client->newweapon) self->owner->client->newweapon = self->owner->client->pers.weapon; CTFResetGrapple(self); return; } if (self->enemy) { if (self->enemy->solid == SOLID_NOT) { CTFResetGrapple(self); return; } if (self->enemy->solid == SOLID_BBOX) { v = self->enemy->size * 0.5f; v += self->enemy->s.origin; self->s.origin = v + self->enemy->mins; gi.linkentity(self); } else self->velocity = self->enemy->velocity; if (self->enemy->deadflag) { // he died CTFResetGrapple(self); return; } } CTFGrappleDrawCable(self); if (self->owner->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY) { // pull player toward grapple vec3_t forward, up; AngleVectors(self->owner->client->v_angle, forward, nullptr, up); v = self->owner->s.origin; v[2] += self->owner->viewheight; hookdir = self->s.origin - v; vlen = hookdir.length(); if (self->owner->client->ctf_grapplestate == CTF_GRAPPLE_STATE_PULL && vlen < 64) { self->owner->client->ctf_grapplestate = CTF_GRAPPLE_STATE_HANG; self->s.sound = gi.soundindex("weapons/grapple/grhang.wav"); } hookdir.normalize(); hookdir = hookdir * g_grapple_pull_speed->value; self->owner->velocity = hookdir; self->owner->flags |= FL_NO_KNOCKBACK; SV_AddGravity(self->owner); } } DIE(grapple_die) (edict_t *self, edict_t *other, edict_t *inflictor, int damage, const vec3_t &point, const mod_t &mod) -> void { if (mod.id == MOD_CRUSH) CTFResetGrapple(self); } bool CTFFireGrapple(edict_t *self, const vec3_t &start, const vec3_t &dir, int damage, int speed, effects_t effect) { edict_t *grapple; trace_t tr; vec3_t normalized = dir.normalized(); grapple = G_Spawn(); grapple->s.origin = start; grapple->s.old_origin = start; grapple->s.angles = vectoangles(normalized); grapple->velocity = normalized * speed; grapple->movetype = MOVETYPE_FLYMISSILE; grapple->clipmask = MASK_PROJECTILE; // [Paril-KEX] if (self->client && !G_ShouldPlayersCollide(true)) grapple->clipmask &= ~CONTENTS_PLAYER; grapple->solid = SOLID_BBOX; grapple->s.effects |= effect; grapple->s.modelindex = gi.modelindex("models/weapons/grapple/hook/tris.md2"); grapple->owner = self; grapple->touch = CTFGrappleTouch; grapple->dmg = damage; grapple->flags |= FL_NO_KNOCKBACK | FL_NO_DAMAGE_EFFECTS; grapple->takedamage = true; grapple->die = grapple_die; self->client->ctf_grapple = grapple; self->client->ctf_grapplestate = CTF_GRAPPLE_STATE_FLY; // we're firing, not on hook gi.linkentity(grapple); tr = gi.traceline(self->s.origin, grapple->s.origin, grapple, grapple->clipmask); if (tr.fraction < 1.0f) { grapple->s.origin = tr.endpos + (tr.plane.normal * 1.f); grapple->touch(grapple, tr.ent, tr, false); return false; } grapple->s.sound = gi.soundindex("weapons/grapple/grfly.wav"); return true; } void CTFGrappleFire(edict_t *ent, const vec3_t &g_offset, int damage, effects_t effect) { float volume = 1.0; if (ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY) return; // it's already out vec3_t start, dir; P_ProjectSource(ent, ent->client->v_angle, vec3_t{ 24, 8, -8 + 2 } + g_offset, start, dir); if (ent->client->silencer_shots) volume = 0.2f; if (CTFFireGrapple(ent, start, dir, damage, g_grapple_fly_speed->value, effect)) gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/grapple/grfire.wav"), volume, ATTN_NORM, 0); PlayerNoise(ent, start, PNOISE_WEAPON); } void CTFWeapon_Grapple_Fire(edict_t *ent) { CTFGrappleFire(ent, vec3_origin, g_grapple_damage->integer, EF_NONE); } void CTFWeapon_Grapple(edict_t *ent) { constexpr int pause_frames[] = { 10, 18, 27, 0 }; constexpr int fire_frames[] = { 6, 0 }; int prevstate; // if the the attack button is still down, stay in the firing frame if ((ent->client->buttons & (BUTTON_ATTACK | BUTTON_HOLSTER)) && ent->client->weaponstate == WEAPON_FIRING && ent->client->ctf_grapple) ent->client->ps.gunframe = 6; if (!(ent->client->buttons & (BUTTON_ATTACK | BUTTON_HOLSTER)) && ent->client->ctf_grapple) { CTFResetGrapple(ent->client->ctf_grapple); if (ent->client->weaponstate == WEAPON_FIRING) ent->client->weaponstate = WEAPON_READY; } if ((ent->client->newweapon || ((ent->client->latched_buttons | ent->client->buttons) & BUTTON_HOLSTER)) && ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY && ent->client->weaponstate == WEAPON_FIRING) { // he wants to change weapons while grappled if (!ent->client->newweapon) ent->client->newweapon = ent->client->pers.weapon; ent->client->weaponstate = WEAPON_DROPPING; ent->client->ps.gunframe = 32; } prevstate = ent->client->weaponstate; Weapon_Generic(ent, 5, 10, 31, 36, pause_frames, fire_frames, CTFWeapon_Grapple_Fire); // if the the attack button is still down, stay in the firing frame if ((ent->client->buttons & (BUTTON_ATTACK | BUTTON_HOLSTER)) && ent->client->weaponstate == WEAPON_FIRING && ent->client->ctf_grapple) ent->client->ps.gunframe = 6; // if we just switched back to grapple, immediately go to fire frame if (prevstate == WEAPON_ACTIVATING && ent->client->weaponstate == WEAPON_READY && ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY) { if (!(ent->client->buttons & (BUTTON_ATTACK | BUTTON_HOLSTER))) ent->client->ps.gunframe = 6; else ent->client->ps.gunframe = 5; ent->client->weaponstate = WEAPON_FIRING; } } void CTFDirtyTeamMenu() { for (auto player : active_players()) if (player->client->menu) { player->client->menudirty = true; player->client->menutime = level.time; } } void CTFTeam_f(edict_t *ent) { if (!G_TeamplayEnabled()) return; const char *t; ctfteam_t desired_team; t = gi.args(); if (!*t) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_you_are_on_team", CTFTeamName(ent->client->resp.ctf_team)); return; } if (ctfgame.match > MATCH_SETUP) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_cant_change_teams"); return; } // [Paril-KEX] with force-join, don't allow us to switch // using this command. if (g_teamplay_force_join->integer) { if (!(ent->svflags & SVF_BOT)) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_cant_change_teams"); return; } } if (Q_strcasecmp(t, "red") == 0) desired_team = CTF_TEAM1; else if (Q_strcasecmp(t, "blue") == 0) desired_team = CTF_TEAM2; else { gi.LocClient_Print(ent, PRINT_HIGH, "$g_unknown_team", t); return; } if (ent->client->resp.ctf_team == desired_team) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_already_on_team", CTFTeamName(ent->client->resp.ctf_team)); return; } //// ent->svflags = SVF_NONE; ent->flags &= ~FL_GODMODE; ent->client->resp.ctf_team = desired_team; ent->client->resp.ctf_state = 0; char value[MAX_INFO_VALUE] = { 0 }; gi.Info_ValueForKey(ent->client->pers.userinfo, "skin", value, sizeof(value)); CTFAssignSkin(ent, value); // if anybody has a menu open, update it immediately CTFDirtyTeamMenu(); if (ent->solid == SOLID_NOT) { // spectator PutClientInServer(ent); G_PostRespawn(ent); gi.LocBroadcast_Print(PRINT_HIGH, "$g_joined_team", ent->client->pers.netname, CTFTeamName(desired_team)); return; } ent->health = 0; player_die(ent, ent, ent, 100000, vec3_origin, { MOD_SUICIDE, true }); // don't even bother waiting for death frames ent->deadflag = true; respawn(ent); ent->client->resp.score = 0; gi.LocBroadcast_Print(PRINT_HIGH, "$g_changed_team", ent->client->pers.netname, CTFTeamName(desired_team)); } constexpr size_t MAX_CTF_STAT_LENGTH = 1024; /* ================== CTFScoreboardMessage ================== */ void CTFScoreboardMessage(edict_t *ent, edict_t *killer) { uint32_t i, j, k, n; uint32_t sorted[2][MAX_CLIENTS]; int32_t sortedscores[2][MAX_CLIENTS]; int score; uint32_t total[2]; int totalscore[2]; uint32_t last[2]; gclient_t *cl; edict_t *cl_ent; int team; // sort the clients by team and score total[0] = total[1] = 0; last[0] = last[1] = 0; totalscore[0] = totalscore[1] = 0; for (i = 0; i < game.maxclients; i++) { cl_ent = g_edicts + 1 + i; if (!cl_ent->inuse) continue; if (game.clients[i].resp.ctf_team == CTF_TEAM1) team = 0; else if (game.clients[i].resp.ctf_team == CTF_TEAM2) team = 1; else continue; // unknown team? score = game.clients[i].resp.score; for (j = 0; j < total[team]; j++) { if (score > sortedscores[team][j]) break; } for (k = total[team]; k > j; k--) { sorted[team][k] = sorted[team][k - 1]; sortedscores[team][k] = sortedscores[team][k - 1]; } sorted[team][j] = i; sortedscores[team][j] = score; totalscore[team] += score; total[team]++; } // print level name and exit rules // add the clients in sorted order static std::string string; string.clear(); // [Paril-KEX] time & frags if (teamplay->integer) { if (fraglimit->integer) { fmt::format_to(std::back_inserter(string), FMT_STRING("xv -20 yv -10 loc_string2 1 $g_score_frags \"{}\" "), fraglimit->integer); } } else { if (capturelimit->integer) { fmt::format_to(std::back_inserter(string), FMT_STRING("xv -20 yv -10 loc_string2 1 $g_score_captures \"{}\" "), capturelimit->integer); } } if (timelimit->value) { fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 time_limit {} "), gi.ServerFrame() + ((gtime_t::from_min(timelimit->value) - level.time)).milliseconds() / gi.frame_time_ms); } // team one if (teamplay->integer) { fmt::format_to(std::back_inserter(string), FMT_STRING("if 25 xv -32 yv 8 pic 25 endif " "xv -123 yv 28 cstring \"{}\" " "xv 41 yv 12 num 3 19 " "if 26 xv 208 yv 8 pic 26 endif " "xv 117 yv 28 cstring \"{}\" " "xv 280 yv 12 num 3 21 "), total[0], total[1]); } else { fmt::format_to(std::back_inserter(string), FMT_STRING("if 25 xv -32 yv 8 pic 25 endif " "xv 0 yv 28 string \"{:4}/{:<3}\" " "xv 58 yv 12 num 2 19 " "if 26 xv 208 yv 8 pic 26 endif " "xv 240 yv 28 string \"{:4}/{:<3}\" " "xv 296 yv 12 num 2 21 "), totalscore[0], total[0], totalscore[1], total[1]); } for (i = 0; i < 16; i++) { if (i >= total[0] && i >= total[1]) break; // we're done // left side if (i < total[0]) { cl = &game.clients[sorted[0][i]]; cl_ent = g_edicts + 1 + sorted[0][i]; std::string_view entry = G_Fmt("ctf -40 {} {} {} {} {} ", 42 + i * 8, sorted[0][i], cl->resp.score, cl->ping > 999 ? 999 : cl->ping, cl_ent->client->pers.inventory[IT_FLAG2] ? "sbfctf2" : "\"\""); if (string.size() + entry.size() < MAX_CTF_STAT_LENGTH) { string += entry; last[0] = i; } } // right side if (i < total[1]) { cl = &game.clients[sorted[1][i]]; cl_ent = g_edicts + 1 + sorted[1][i]; std::string_view entry = G_Fmt("ctf 200 {} {} {} {} {} ", 42 + i * 8, sorted[1][i], cl->resp.score, cl->ping > 999 ? 999 : cl->ping, cl_ent->client->pers.inventory[IT_FLAG1] ? "sbfctf1" : "\"\""); if (string.size() + entry.size() < MAX_CTF_STAT_LENGTH) { string += entry; last[1] = i; } } } // put in spectators if we have enough room if (last[0] > last[1]) j = last[0]; else j = last[1]; j = (j + 2) * 8 + 42; k = n = 0; if (string.size() < MAX_CTF_STAT_LENGTH - 50) { for (i = 0; i < game.maxclients; i++) { cl_ent = g_edicts + 1 + i; cl = &game.clients[i]; if (!cl_ent->inuse || cl_ent->solid != SOLID_NOT || cl_ent->client->resp.ctf_team != CTF_NOTEAM) continue; if (!k) { k = 1; fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv {} loc_string2 0 \"$g_pc_spectators\" "), j); j += 8; } std::string_view entry = G_Fmt("ctf {} {} {} {} {} \"\" ", (n & 1) ? 200 : -40, // x j, // y i, // playernum cl->resp.score, cl->ping > 999 ? 999 : cl->ping); if (string.size() + entry.size() < MAX_CTF_STAT_LENGTH) string += entry; if (n & 1) j += 8; n++; } } if (total[0] - last[0] > 1) // couldn't fit everyone fmt::format_to(std::back_inserter(string), FMT_STRING("xv -32 yv {} loc_string 1 $g_ctf_and_more {} "), 42 + (last[0] + 1) * 8, total[0] - last[0] - 1); if (total[1] - last[1] > 1) // couldn't fit everyone fmt::format_to(std::back_inserter(string), FMT_STRING("xv 208 yv {} loc_string 1 $g_ctf_and_more {} "), 42 + (last[1] + 1) * 8, total[1] - last[1] - 1); if (level.intermissiontime) fmt::format_to(std::back_inserter(string), FMT_STRING("ifgef {} yb -48 xv 0 loc_cstring2 0 \"$m_eou_press_button\" endif "), (level.intermission_server_frame + (5_sec).frames())); gi.WriteByte(svc_layout); gi.WriteString(string.c_str()); } /*------------------------------------------------------------------------*/ /* TECH */ /*------------------------------------------------------------------------*/ void CTFHasTech(edict_t *who) { if (level.time - who->client->ctf_lasttechmsg > 2_sec) { gi.LocCenter_Print(who, "$g_already_have_tech"); who->client->ctf_lasttechmsg = level.time; } } gitem_t *CTFWhat_Tech(edict_t *ent) { int i; i = 0; for (; i < q_countof(tech_ids); i++) { if (ent->client->pers.inventory[tech_ids[i]]) { return GetItemByIndex(tech_ids[i]); } } return nullptr; } bool CTFPickup_Tech(edict_t *ent, edict_t *other) { int i; i = 0; for (; i < q_countof(tech_ids); i++) { if (other->client->pers.inventory[tech_ids[i]]) { CTFHasTech(other); return false; // has this one } } // client only gets one tech other->client->pers.inventory[ent->item->id]++; other->client->ctf_regentime = level.time; return true; } static void SpawnTech(gitem_t *item, edict_t *spot); static edict_t *FindTechSpawn() { return SelectDeathmatchSpawnPoint(false, true, true).spot; } THINK(TechThink) (edict_t *tech) -> void { edict_t *spot; if ((spot = FindTechSpawn()) != nullptr) { SpawnTech(tech->item, spot); G_FreeEdict(tech); } else { tech->nextthink = level.time + CTF_TECH_TIMEOUT; tech->think = TechThink; } } void CTFDrop_Tech(edict_t *ent, gitem_t *item) { edict_t *tech; tech = Drop_Item(ent, item); tech->nextthink = level.time + CTF_TECH_TIMEOUT; tech->think = TechThink; ent->client->pers.inventory[item->id] = 0; } void CTFDeadDropTech(edict_t *ent) { edict_t *dropped; int i; i = 0; for (; i < q_countof(tech_ids); i++) { if (ent->client->pers.inventory[tech_ids[i]]) { dropped = Drop_Item(ent, GetItemByIndex(tech_ids[i])); // hack the velocity to make it bounce random dropped->velocity[0] = crandom_open() * 300; dropped->velocity[1] = crandom_open() * 300; dropped->nextthink = level.time + CTF_TECH_TIMEOUT; dropped->think = TechThink; dropped->owner = nullptr; ent->client->pers.inventory[tech_ids[i]] = 0; } } } static void SpawnTech(gitem_t *item, edict_t *spot) { edict_t *ent; vec3_t forward, right; vec3_t angles; ent = G_Spawn(); ent->classname = item->classname; ent->item = item; ent->spawnflags = SPAWNFLAG_ITEM_DROPPED; ent->s.effects = item->world_model_flags; ent->s.renderfx = RF_GLOW | RF_NO_LOD; ent->mins = { -15, -15, -15 }; ent->maxs = { 15, 15, 15 }; gi.setmodel(ent, ent->item->world_model); ent->solid = SOLID_TRIGGER; ent->movetype = MOVETYPE_TOSS; ent->touch = Touch_Item; ent->owner = ent; angles[0] = 0; angles[1] = (float) irandom(360); angles[2] = 0; AngleVectors(angles, forward, right, nullptr); ent->s.origin = spot->s.origin; ent->s.origin[2] += 16; ent->velocity = forward * 100; ent->velocity[2] = 300; ent->nextthink = level.time + CTF_TECH_TIMEOUT; ent->think = TechThink; gi.linkentity(ent); } THINK(SpawnTechs) (edict_t *ent) -> void { edict_t *spot; int i; i = 0; for (; i < q_countof(tech_ids); i++) { if ((spot = FindTechSpawn()) != nullptr) SpawnTech(GetItemByIndex(tech_ids[i]), spot); } if (ent) G_FreeEdict(ent); } // frees the passed edict! void CTFRespawnTech(edict_t *ent) { edict_t *spot; if ((spot = FindTechSpawn()) != nullptr) SpawnTech(ent->item, spot); G_FreeEdict(ent); } void CTFSetupTechSpawn() { edict_t *ent; bool techs_allowed; // [Paril-KEX] if (!strcmp(g_allow_techs->string, "auto")) techs_allowed = !!ctf->integer; else techs_allowed = !!g_allow_techs->integer; if (!techs_allowed) return; ent = G_Spawn(); ent->nextthink = level.time + 2_sec; ent->think = SpawnTechs; } void CTFResetTech() { edict_t *ent; uint32_t i; for (ent = g_edicts + 1, i = 1; i < globals.num_edicts; i++, ent++) { if (ent->inuse) if (ent->item && (ent->item->flags & IF_TECH)) G_FreeEdict(ent); } SpawnTechs(nullptr); } int CTFApplyResistance(edict_t *ent, int dmg) { float volume = 1.0; if (ent->client && ent->client->silencer_shots) volume = 0.2f; if (dmg && ent->client && ent->client->pers.inventory[IT_TECH_RESISTANCE]) { // make noise gi.sound(ent, CHAN_AUX, gi.soundindex("ctf/tech1.wav"), volume, ATTN_NORM, 0); return dmg / 2; } return dmg; } int CTFApplyStrength(edict_t *ent, int dmg) { if (dmg && ent->client && ent->client->pers.inventory[IT_TECH_STRENGTH]) { return dmg * 2; } return dmg; } bool CTFApplyStrengthSound(edict_t *ent) { float volume = 1.0; if (ent->client && ent->client->silencer_shots) volume = 0.2f; if (ent->client && ent->client->pers.inventory[IT_TECH_STRENGTH]) { if (ent->client->ctf_techsndtime < level.time) { ent->client->ctf_techsndtime = level.time + 1_sec; if (ent->client->quad_time > level.time) gi.sound(ent, CHAN_AUX, gi.soundindex("ctf/tech2x.wav"), volume, ATTN_NORM, 0); else gi.sound(ent, CHAN_AUX, gi.soundindex("ctf/tech2.wav"), volume, ATTN_NORM, 0); } return true; } return false; } bool CTFApplyHaste(edict_t *ent) { if (ent->client && ent->client->pers.inventory[IT_TECH_HASTE]) return true; return false; } void CTFApplyHasteSound(edict_t *ent) { float volume = 1.0; if (ent->client && ent->client->silencer_shots) volume = 0.2f; if (ent->client && ent->client->pers.inventory[IT_TECH_HASTE] && ent->client->ctf_techsndtime < level.time) { ent->client->ctf_techsndtime = level.time + 1_sec; gi.sound(ent, CHAN_AUX, gi.soundindex("ctf/tech3.wav"), volume, ATTN_NORM, 0); } } void CTFApplyRegeneration(edict_t *ent) { bool noise = false; gclient_t *client; int index; float volume = 1.0; client = ent->client; if (!client) return; if (ent->client->silencer_shots) volume = 0.2f; if (client->pers.inventory[IT_TECH_REGENERATION]) { if (client->ctf_regentime < level.time) { client->ctf_regentime = level.time; if (ent->health < 150) { ent->health += 5; if (ent->health > 150) ent->health = 150; client->ctf_regentime += 500_ms; noise = true; } index = ArmorIndex(ent); if (index && client->pers.inventory[index] < 150) { client->pers.inventory[index] += 5; if (client->pers.inventory[index] > 150) client->pers.inventory[index] = 150; client->ctf_regentime += 500_ms; noise = true; } } if (noise && ent->client->ctf_techsndtime < level.time) { ent->client->ctf_techsndtime = level.time + 1_sec; gi.sound(ent, CHAN_AUX, gi.soundindex("ctf/tech4.wav"), volume, ATTN_NORM, 0); } } } bool CTFHasRegeneration(edict_t *ent) { if (ent->client && ent->client->pers.inventory[IT_TECH_REGENERATION]) return true; return false; } void CTFSay_Team(edict_t *who, const char *msg_in) { edict_t *cl_ent; char outmsg[256]; if (CheckFlood(who)) return; Q_strlcpy(outmsg, msg_in, sizeof(outmsg)); char *msg = outmsg; if (*msg == '\"') { msg[strlen(msg) - 1] = 0; msg++; } for (uint32_t i = 0; i < game.maxclients; i++) { cl_ent = g_edicts + 1 + i; if (!cl_ent->inuse) continue; if (cl_ent->client->resp.ctf_team == who->client->resp.ctf_team) gi.LocClient_Print(cl_ent, PRINT_CHAT, "({}): {}\n", who->client->pers.netname, msg); } } /*-----------------------------------------------------------------------*/ /*QUAKED misc_ctf_banner (1 .5 0) (-4 -64 0) (4 64 248) TEAM2 The origin is the bottom of the banner. The banner is 248 tall. */ THINK(misc_ctf_banner_think) (edict_t *ent) -> void { ent->s.frame = (ent->s.frame + 1) % 16; ent->nextthink = level.time + 10_hz; } constexpr spawnflags_t SPAWNFLAG_CTF_BANNER_BLUE = 1_spawnflag; void SP_misc_ctf_banner(edict_t *ent) { ent->movetype = MOVETYPE_NONE; ent->solid = SOLID_NOT; ent->s.modelindex = gi.modelindex("models/ctf/banner/tris.md2"); if (ent->spawnflags.has(SPAWNFLAG_CTF_BANNER_BLUE)) // team2 ent->s.skinnum = 1; ent->s.frame = irandom(16); gi.linkentity(ent); ent->think = misc_ctf_banner_think; ent->nextthink = level.time + 10_hz; } /*QUAKED misc_ctf_small_banner (1 .5 0) (-4 -32 0) (4 32 124) TEAM2 The origin is the bottom of the banner. The banner is 124 tall. */ void SP_misc_ctf_small_banner(edict_t *ent) { ent->movetype = MOVETYPE_NONE; ent->solid = SOLID_NOT; ent->s.modelindex = gi.modelindex("models/ctf/banner/small.md2"); if (ent->spawnflags.has(SPAWNFLAG_CTF_BANNER_BLUE)) // team2 ent->s.skinnum = 1; ent->s.frame = irandom(16); gi.linkentity(ent); ent->think = misc_ctf_banner_think; ent->nextthink = level.time + 10_hz; } /*-----------------------------------------------------------------------*/ static void SetGameName(pmenu_t *p) { if (ctf->integer) Q_strlcpy(p->text, "$g_pc_3wctf", sizeof(p->text)); else Q_strlcpy(p->text, "$g_pc_teamplay", sizeof(p->text)); } static void SetLevelName(pmenu_t *p) { static char levelname[33]; levelname[0] = '*'; if (g_edicts[0].message) Q_strlcpy(levelname + 1, g_edicts[0].message, sizeof(levelname) - 1); else Q_strlcpy(levelname + 1, level.mapname, sizeof(levelname) - 1); levelname[sizeof(levelname) - 1] = 0; Q_strlcpy(p->text, levelname, sizeof(p->text)); } /*-----------------------------------------------------------------------*/ /* ELECTIONS */ bool CTFBeginElection(edict_t *ent, elect_t type, const char *msg) { int count; edict_t *e; if (electpercentage->value == 0) { gi.LocClient_Print(ent, PRINT_HIGH, "Elections are disabled, only an admin can process this action.\n"); return false; } if (ctfgame.election != ELECT_NONE) { gi.LocClient_Print(ent, PRINT_HIGH, "Election already in progress.\n"); return false; } // clear votes count = 0; for (uint32_t i = 1; i <= game.maxclients; i++) { e = g_edicts + i; e->client->resp.voted = false; if (e->inuse) count++; } if (count < 2) { gi.LocClient_Print(ent, PRINT_HIGH, "Not enough players for election.\n"); return false; } ctfgame.etarget = ent; ctfgame.election = type; ctfgame.evotes = 0; ctfgame.needvotes = (int) ((count * electpercentage->value) / 100); ctfgame.electtime = level.time + 20_sec; // twenty seconds for election Q_strlcpy(ctfgame.emsg, msg, sizeof(ctfgame.emsg)); // tell everyone gi.Broadcast_Print(PRINT_CHAT, ctfgame.emsg); gi.LocBroadcast_Print(PRINT_HIGH, "Type YES or NO to vote on this request.\n"); gi.LocBroadcast_Print(PRINT_HIGH, "Votes: {} Needed: {} Time left: {}s\n", ctfgame.evotes, ctfgame.needvotes, (ctfgame.electtime - level.time).seconds()); return true; } void DoRespawn(edict_t *ent); void CTFResetAllPlayers() { uint32_t i; edict_t *ent; for (i = 1; i <= game.maxclients; i++) { ent = g_edicts + i; if (!ent->inuse) continue; if (ent->client->menu) PMenu_Close(ent); CTFPlayerResetGrapple(ent); CTFDeadDropFlag(ent); CTFDeadDropTech(ent); ent->client->resp.ctf_team = CTF_NOTEAM; ent->client->resp.ready = false; ent->svflags = SVF_NONE; ent->flags &= ~FL_GODMODE; PutClientInServer(ent); } // reset the level CTFResetTech(); CTFResetFlags(); for (ent = g_edicts + 1, i = 1; i < globals.num_edicts; i++, ent++) { if (ent->inuse && !ent->client) { if (ent->solid == SOLID_NOT && ent->think == DoRespawn && ent->nextthink >= level.time) { ent->nextthink = 0_ms; DoRespawn(ent); } } } if (ctfgame.match == MATCH_SETUP) ctfgame.matchtime = level.time + gtime_t::from_min(matchsetuptime->value); } void CTFAssignGhost(edict_t *ent) { int ghost, i; for (ghost = 0; ghost < MAX_CLIENTS; ghost++) if (!ctfgame.ghosts[ghost].code) break; if (ghost == MAX_CLIENTS) return; ctfgame.ghosts[ghost].team = ent->client->resp.ctf_team; ctfgame.ghosts[ghost].score = 0; for (;;) { ctfgame.ghosts[ghost].code = irandom(10000, 100000); for (i = 0; i < MAX_CLIENTS; i++) if (i != ghost && ctfgame.ghosts[i].code == ctfgame.ghosts[ghost].code) break; if (i == MAX_CLIENTS) break; } ctfgame.ghosts[ghost].ent = ent; Q_strlcpy(ctfgame.ghosts[ghost].netname, ent->client->pers.netname, sizeof(ctfgame.ghosts[ghost].netname)); ent->client->resp.ghost = ctfgame.ghosts + ghost; gi.LocClient_Print(ent, PRINT_CHAT, "Your ghost code is **** {} ****\n", ctfgame.ghosts[ghost].code); gi.LocClient_Print(ent, PRINT_HIGH, "If you lose connection, you can rejoin with your score intact by typing \"ghost {}\".\n", ctfgame.ghosts[ghost].code); } // start a match void CTFStartMatch() { edict_t *ent; ctfgame.match = MATCH_GAME; ctfgame.matchtime = level.time + gtime_t::from_min(matchtime->value); ctfgame.countdown = false; ctfgame.team1 = ctfgame.team2 = 0; memset(ctfgame.ghosts, 0, sizeof(ctfgame.ghosts)); for (uint32_t i = 1; i <= game.maxclients; i++) { ent = g_edicts + i; if (!ent->inuse) continue; ent->client->resp.score = 0; ent->client->resp.ctf_state = 0; ent->client->resp.ghost = nullptr; gi.LocCenter_Print(ent, "******************\n\nMATCH HAS STARTED!\n\n******************"); if (ent->client->resp.ctf_team != CTF_NOTEAM) { // make up a ghost code CTFAssignGhost(ent); CTFPlayerResetGrapple(ent); ent->svflags = SVF_NOCLIENT; ent->flags &= ~FL_GODMODE; ent->client->respawn_time = level.time + random_time(1_sec, 4_sec); ent->client->ps.pmove.pm_type = PM_DEAD; ent->client->anim_priority = ANIM_DEATH; ent->s.frame = FRAME_death308 - 1; ent->client->anim_end = FRAME_death308; ent->deadflag = true; ent->movetype = MOVETYPE_NOCLIP; ent->client->ps.gunindex = 0; ent->client->ps.gunskin = 0; gi.linkentity(ent); } } } void CTFEndMatch() { ctfgame.match = MATCH_POST; gi.LocBroadcast_Print(PRINT_CHAT, "MATCH COMPLETED!\n"); CTFCalcScores(); gi.LocBroadcast_Print(PRINT_HIGH, "RED TEAM: {} captures, {} points\n", ctfgame.team1, ctfgame.total1); gi.LocBroadcast_Print(PRINT_HIGH, "BLUE TEAM: {} captures, {} points\n", ctfgame.team2, ctfgame.total2); if (ctfgame.team1 > ctfgame.team2) gi.LocBroadcast_Print(PRINT_CHAT, "$g_ctf_red_wins_caps", ctfgame.team1 - ctfgame.team2); else if (ctfgame.team2 > ctfgame.team1) gi.LocBroadcast_Print(PRINT_CHAT, "$g_ctf_blue_wins_caps", ctfgame.team2 - ctfgame.team1); else if (ctfgame.total1 > ctfgame.total2) // frag tie breaker gi.LocBroadcast_Print(PRINT_CHAT, "$g_ctf_red_wins_points", ctfgame.total1 - ctfgame.total2); else if (ctfgame.total2 > ctfgame.total1) gi.LocBroadcast_Print(PRINT_CHAT, "$g_ctf_blue_wins_points", ctfgame.total2 - ctfgame.total1); else gi.LocBroadcast_Print(PRINT_CHAT, "$g_ctf_tie_game"); EndDMLevel(); } bool CTFNextMap() { if (ctfgame.match == MATCH_POST) { ctfgame.match = MATCH_SETUP; CTFResetAllPlayers(); return true; } return false; } void CTFWinElection() { switch (ctfgame.election) { case ELECT_MATCH: // reset into match mode if (competition->integer < 3) gi.cvar_set("competition", "2"); ctfgame.match = MATCH_SETUP; CTFResetAllPlayers(); break; case ELECT_ADMIN: ctfgame.etarget->client->resp.admin = true; gi.LocBroadcast_Print(PRINT_HIGH, "{} has become an admin.\n", ctfgame.etarget->client->pers.netname); gi.LocClient_Print(ctfgame.etarget, PRINT_HIGH, "Type 'admin' to access the adminstration menu.\n"); break; case ELECT_MAP: gi.LocBroadcast_Print(PRINT_HIGH, "{} is warping to level {}.\n", ctfgame.etarget->client->pers.netname, ctfgame.elevel); Q_strlcpy(level.forcemap, ctfgame.elevel, sizeof(level.forcemap)); EndDMLevel(); break; default: break; } ctfgame.election = ELECT_NONE; } void CTFVoteYes(edict_t *ent) { if (ctfgame.election == ELECT_NONE) { gi.LocClient_Print(ent, PRINT_HIGH, "No election is in progress.\n"); return; } if (ent->client->resp.voted) { gi.LocClient_Print(ent, PRINT_HIGH, "You already voted.\n"); return; } if (ctfgame.etarget == ent) { gi.LocClient_Print(ent, PRINT_HIGH, "You can't vote for yourself.\n"); return; } ent->client->resp.voted = true; ctfgame.evotes++; if (ctfgame.evotes == ctfgame.needvotes) { // the election has been won CTFWinElection(); return; } gi.LocBroadcast_Print(PRINT_HIGH, "{}\n", ctfgame.emsg); gi.LocBroadcast_Print(PRINT_CHAT, "Votes: {} Needed: {} Time left: {}s\n", ctfgame.evotes, ctfgame.needvotes, (ctfgame.electtime - level.time).seconds()); } void CTFVoteNo(edict_t *ent) { if (ctfgame.election == ELECT_NONE) { gi.LocClient_Print(ent, PRINT_HIGH, "No election is in progress.\n"); return; } if (ent->client->resp.voted) { gi.LocClient_Print(ent, PRINT_HIGH, "You already voted.\n"); return; } if (ctfgame.etarget == ent) { gi.LocClient_Print(ent, PRINT_HIGH, "You can't vote for yourself.\n"); return; } ent->client->resp.voted = true; gi.LocBroadcast_Print(PRINT_HIGH, "{}\n", ctfgame.emsg); gi.LocBroadcast_Print(PRINT_CHAT, "Votes: {} Needed: {} Time left: {}s\n", ctfgame.evotes, ctfgame.needvotes, (ctfgame.electtime - level.time).seconds()); } void CTFReady(edict_t *ent) { uint32_t i, j; edict_t *e; uint32_t t1, t2; if (ent->client->resp.ctf_team == CTF_NOTEAM) { gi.LocClient_Print(ent, PRINT_HIGH, "Pick a team first (hit for menu)\n"); return; } if (ctfgame.match != MATCH_SETUP) { gi.LocClient_Print(ent, PRINT_HIGH, "A match is not being setup.\n"); return; } if (ent->client->resp.ready) { gi.LocClient_Print(ent, PRINT_HIGH, "You have already commited.\n"); return; } ent->client->resp.ready = true; gi.LocBroadcast_Print(PRINT_HIGH, "{} is ready.\n", ent->client->pers.netname); t1 = t2 = 0; for (j = 0, i = 1; i <= game.maxclients; i++) { e = g_edicts + i; if (!e->inuse) continue; if (e->client->resp.ctf_team != CTF_NOTEAM && !e->client->resp.ready) j++; if (e->client->resp.ctf_team == CTF_TEAM1) t1++; else if (e->client->resp.ctf_team == CTF_TEAM2) t2++; } if (!j && t1 && t2) { // everyone has commited gi.LocBroadcast_Print(PRINT_CHAT, "All players have committed. Match starting\n"); ctfgame.match = MATCH_PREGAME; ctfgame.matchtime = level.time + gtime_t::from_sec(matchstarttime->value); ctfgame.countdown = false; gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("misc/talk1.wav"), 1, ATTN_NONE, 0); } } void CTFNotReady(edict_t *ent) { if (ent->client->resp.ctf_team == CTF_NOTEAM) { gi.LocClient_Print(ent, PRINT_HIGH, "Pick a team first (hit for menu)\n"); return; } if (ctfgame.match != MATCH_SETUP && ctfgame.match != MATCH_PREGAME) { gi.LocClient_Print(ent, PRINT_HIGH, "A match is not being setup.\n"); return; } if (!ent->client->resp.ready) { gi.LocClient_Print(ent, PRINT_HIGH, "You haven't commited.\n"); return; } ent->client->resp.ready = false; gi.LocBroadcast_Print(PRINT_HIGH, "{} is no longer ready.\n", ent->client->pers.netname); if (ctfgame.match == MATCH_PREGAME) { gi.LocBroadcast_Print(PRINT_CHAT, "Match halted.\n"); ctfgame.match = MATCH_SETUP; ctfgame.matchtime = level.time + gtime_t::from_min(matchsetuptime->value); } } void CTFGhost(edict_t *ent) { int i; int n; if (gi.argc() < 2) { gi.LocClient_Print(ent, PRINT_HIGH, "Usage: ghost \n"); return; } if (ent->client->resp.ctf_team != CTF_NOTEAM) { gi.LocClient_Print(ent, PRINT_HIGH, "You are already in the game.\n"); return; } if (ctfgame.match != MATCH_GAME) { gi.LocClient_Print(ent, PRINT_HIGH, "No match is in progress.\n"); return; } n = atoi(gi.argv(1)); for (i = 0; i < MAX_CLIENTS; i++) { if (ctfgame.ghosts[i].code && ctfgame.ghosts[i].code == n) { gi.LocClient_Print(ent, PRINT_HIGH, "Ghost code accepted, your position has been reinstated.\n"); ctfgame.ghosts[i].ent->client->resp.ghost = nullptr; ent->client->resp.ctf_team = ctfgame.ghosts[i].team; ent->client->resp.ghost = ctfgame.ghosts + i; ent->client->resp.score = ctfgame.ghosts[i].score; ent->client->resp.ctf_state = 0; ctfgame.ghosts[i].ent = ent; ent->svflags = SVF_NONE; ent->flags &= ~FL_GODMODE; PutClientInServer(ent); gi.LocBroadcast_Print(PRINT_HIGH, "{} has been reinstated to {} team.\n", ent->client->pers.netname, CTFTeamName(ent->client->resp.ctf_team)); return; } } gi.LocClient_Print(ent, PRINT_HIGH, "Invalid ghost code.\n"); } bool CTFMatchSetup() { if (ctfgame.match == MATCH_SETUP || ctfgame.match == MATCH_PREGAME) return true; return false; } bool CTFMatchOn() { if (ctfgame.match == MATCH_GAME) return true; return false; } /*-----------------------------------------------------------------------*/ void CTFJoinTeam1(edict_t *ent, pmenuhnd_t *p); void CTFJoinTeam2(edict_t *ent, pmenuhnd_t *p); void CTFReturnToMain(edict_t *ent, pmenuhnd_t *p); void CTFChaseCam(edict_t *ent, pmenuhnd_t *p); static const int jmenu_level = 1; static const int jmenu_match = 2; static const int jmenu_red = 4; static const int jmenu_blue = 7; static const int jmenu_chase = 10; static const int jmenu_reqmatch = 12; const pmenu_t joinmenu[] = { { "*$g_pc_3wctf", PMENU_ALIGN_CENTER, nullptr }, { "", PMENU_ALIGN_CENTER, nullptr }, { "", PMENU_ALIGN_CENTER, nullptr }, { "", PMENU_ALIGN_CENTER, nullptr }, { "$g_pc_join_red_team", PMENU_ALIGN_LEFT, CTFJoinTeam1 }, { "", PMENU_ALIGN_LEFT, nullptr }, { "", PMENU_ALIGN_LEFT, nullptr }, { "$g_pc_join_blue_team", PMENU_ALIGN_LEFT, CTFJoinTeam2 }, { "", PMENU_ALIGN_LEFT, nullptr }, { "", PMENU_ALIGN_LEFT, nullptr }, { "$g_pc_chase_camera", PMENU_ALIGN_LEFT, CTFChaseCam }, { "", PMENU_ALIGN_LEFT, nullptr }, { "", PMENU_ALIGN_LEFT, nullptr }, }; const pmenu_t nochasemenu[] = { { "$g_pc_3wctf", PMENU_ALIGN_CENTER, nullptr }, { "", PMENU_ALIGN_CENTER, nullptr }, { "", PMENU_ALIGN_CENTER, nullptr }, { "$g_pc_no_chase", PMENU_ALIGN_LEFT, nullptr }, { "", PMENU_ALIGN_CENTER, nullptr }, { "$g_pc_return", PMENU_ALIGN_LEFT, CTFReturnToMain } }; void CTFJoinTeam(edict_t *ent, ctfteam_t desired_team) { PMenu_Close(ent); ent->svflags &= ~SVF_NOCLIENT; ent->client->resp.ctf_team = desired_team; ent->client->resp.ctf_state = 0; char value[MAX_INFO_VALUE] = { 0 }; gi.Info_ValueForKey(ent->client->pers.userinfo, "skin", value, sizeof(value)); CTFAssignSkin(ent, value); // assign a ghost if we are in match mode if (ctfgame.match == MATCH_GAME) { if (ent->client->resp.ghost) ent->client->resp.ghost->code = 0; ent->client->resp.ghost = nullptr; CTFAssignGhost(ent); } PutClientInServer(ent); G_PostRespawn(ent); gi.LocBroadcast_Print(PRINT_HIGH, "$g_joined_team", ent->client->pers.netname, CTFTeamName(desired_team)); if (ctfgame.match == MATCH_SETUP) { gi.LocCenter_Print(ent, "Type \"ready\" in console to ready up.\n"); } // if anybody has a menu open, update it immediately CTFDirtyTeamMenu(); } void CTFJoinTeam1(edict_t *ent, pmenuhnd_t *p) { CTFJoinTeam(ent, CTF_TEAM1); } void CTFJoinTeam2(edict_t *ent, pmenuhnd_t *p) { CTFJoinTeam(ent, CTF_TEAM2); } static void CTFNoChaseCamUpdate(edict_t *ent) { pmenu_t *entries = ent->client->menu->entries; SetGameName(&entries[0]); SetLevelName(&entries[jmenu_level]); } void CTFChaseCam(edict_t *ent, pmenuhnd_t *p) { edict_t *e; CTFJoinTeam(ent, CTF_NOTEAM); if (ent->client->chase_target) { ent->client->chase_target = nullptr; ent->client->ps.pmove.pm_flags &= ~(PMF_NO_POSITIONAL_PREDICTION | PMF_NO_ANGULAR_PREDICTION); PMenu_Close(ent); return; } for (uint32_t i = 1; i <= game.maxclients; i++) { e = g_edicts + i; if (e->inuse && e->solid != SOLID_NOT) { ent->client->chase_target = e; PMenu_Close(ent); ent->client->update_chase = true; return; } } PMenu_Close(ent); PMenu_Open(ent, nochasemenu, -1, sizeof(nochasemenu) / sizeof(pmenu_t), nullptr, CTFNoChaseCamUpdate); } void CTFReturnToMain(edict_t *ent, pmenuhnd_t *p) { PMenu_Close(ent); CTFOpenJoinMenu(ent); } void CTFRequestMatch(edict_t *ent, pmenuhnd_t *p) { PMenu_Close(ent); CTFBeginElection(ent, ELECT_MATCH, G_Fmt("{} has requested to switch to competition mode.\n", ent->client->pers.netname).data()); } void DeathmatchScoreboard(edict_t *ent); void CTFShowScores(edict_t *ent, pmenu_t *p) { PMenu_Close(ent); ent->client->showscores = true; ent->client->showinventory = false; DeathmatchScoreboard(ent); } void CTFUpdateJoinMenu(edict_t *ent) { pmenu_t *entries = ent->client->menu->entries; SetGameName(entries); if (ctfgame.match >= MATCH_PREGAME && matchlock->integer) { Q_strlcpy(entries[jmenu_red].text, "MATCH IS LOCKED", sizeof(entries[jmenu_red].text)); entries[jmenu_red].SelectFunc = nullptr; Q_strlcpy(entries[jmenu_blue].text, " (entry is not permitted)", sizeof(entries[jmenu_blue].text)); entries[jmenu_blue].SelectFunc = nullptr; } else { if (ctfgame.match >= MATCH_PREGAME) { Q_strlcpy(entries[jmenu_red].text, "Join Red MATCH Team", sizeof(entries[jmenu_red].text)); Q_strlcpy(entries[jmenu_blue].text, "Join Blue MATCH Team", sizeof(entries[jmenu_blue].text)); } else { Q_strlcpy(entries[jmenu_red].text, "$g_pc_join_red_team", sizeof(entries[jmenu_red].text)); Q_strlcpy(entries[jmenu_blue].text, "$g_pc_join_blue_team", sizeof(entries[jmenu_blue].text)); } entries[jmenu_red].SelectFunc = CTFJoinTeam1; entries[jmenu_blue].SelectFunc = CTFJoinTeam2; } // KEX_FIXME: what's this for? if (g_teamplay_force_join->string && *g_teamplay_force_join->string) { if (Q_strcasecmp(g_teamplay_force_join->string, "red") == 0) { entries[jmenu_blue].text[0] = '\0'; entries[jmenu_blue].SelectFunc = nullptr; } else if (Q_strcasecmp(g_teamplay_force_join->string, "blue") == 0) { entries[jmenu_red].text[0] = '\0'; entries[jmenu_red].SelectFunc = nullptr; } } if (ent->client->chase_target) Q_strlcpy(entries[jmenu_chase].text, "$g_pc_leave_chase_camera", sizeof(entries[jmenu_chase].text)); else Q_strlcpy(entries[jmenu_chase].text, "$g_pc_chase_camera", sizeof(entries[jmenu_chase].text)); SetLevelName(entries + jmenu_level); uint32_t num1 = 0, num2 = 0; for (uint32_t i = 0; i < game.maxclients; i++) { if (!g_edicts[i + 1].inuse) continue; if (game.clients[i].resp.ctf_team == CTF_TEAM1) num1++; else if (game.clients[i].resp.ctf_team == CTF_TEAM2) num2++; } switch (ctfgame.match) { case MATCH_NONE: entries[jmenu_match].text[0] = '\0'; break; case MATCH_SETUP: Q_strlcpy(entries[jmenu_match].text, "*MATCH SETUP IN PROGRESS", sizeof(entries[jmenu_match].text)); break; case MATCH_PREGAME: Q_strlcpy(entries[jmenu_match].text, "*MATCH STARTING", sizeof(entries[jmenu_match].text)); break; case MATCH_GAME: Q_strlcpy(entries[jmenu_match].text, "*MATCH IN PROGRESS", sizeof(entries[jmenu_match].text)); break; default: break; } if (*entries[jmenu_red].text) { Q_strlcpy(entries[jmenu_red + 1].text, "$g_pc_playercount", sizeof(entries[jmenu_red + 1].text)); G_FmtTo(entries[jmenu_red + 1].text_arg1, "{}", num1); } else { entries[jmenu_red + 1].text[0] = '\0'; entries[jmenu_red + 1].text_arg1[0] = '\0'; } if (*entries[jmenu_blue].text) { Q_strlcpy(entries[jmenu_blue + 1].text, "$g_pc_playercount", sizeof(entries[jmenu_blue + 1].text)); G_FmtTo(entries[jmenu_blue + 1].text_arg1, "{}", num2); } else { entries[jmenu_blue + 1].text[0] = '\0'; entries[jmenu_blue + 1].text_arg1[0] = '\0'; } entries[jmenu_reqmatch].text[0] = '\0'; entries[jmenu_reqmatch].SelectFunc = nullptr; if (competition->integer && ctfgame.match < MATCH_SETUP) { Q_strlcpy(entries[jmenu_reqmatch].text, "Request Match", sizeof(entries[jmenu_reqmatch].text)); entries[jmenu_reqmatch].SelectFunc = CTFRequestMatch; } } void CTFOpenJoinMenu(edict_t *ent) { uint32_t num1 = 0, num2 = 0; for (uint32_t i = 0; i < game.maxclients; i++) { if (!g_edicts[i + 1].inuse) continue; if (game.clients[i].resp.ctf_team == CTF_TEAM1) num1++; else if (game.clients[i].resp.ctf_team == CTF_TEAM2) num2++; } int team; if (num1 > num2) team = CTF_TEAM1; else if (num2 > num1) team = CTF_TEAM2; team = brandom() ? CTF_TEAM1 : CTF_TEAM2; PMenu_Open(ent, joinmenu, team, sizeof(joinmenu) / sizeof(pmenu_t), nullptr, CTFUpdateJoinMenu); } bool CTFStartClient(edict_t *ent) { if (!G_TeamplayEnabled()) return false; if (ent->client->resp.ctf_team != CTF_NOTEAM) return false; if ((!(ent->svflags & SVF_BOT) && !g_teamplay_force_join->integer) || ctfgame.match >= MATCH_SETUP) { // start as 'observer' ent->movetype = MOVETYPE_NOCLIP; ent->solid = SOLID_NOT; ent->svflags |= SVF_NOCLIENT; ent->client->resp.ctf_team = CTF_NOTEAM; ent->client->resp.spectator = true; ent->client->ps.gunindex = 0; ent->client->ps.gunskin = 0; gi.linkentity(ent); CTFOpenJoinMenu(ent); return true; } return false; } void CTFObserver(edict_t *ent) { if (!G_TeamplayEnabled()) return; // start as 'observer' if (ent->movetype == MOVETYPE_NOCLIP) CTFPlayerResetGrapple(ent); CTFDeadDropFlag(ent); CTFDeadDropTech(ent); ent->deadflag = false; ent->movetype = MOVETYPE_NOCLIP; ent->solid = SOLID_NOT; ent->svflags |= SVF_NOCLIENT; ent->client->resp.ctf_team = CTF_NOTEAM; ent->client->ps.gunindex = 0; ent->client->ps.gunskin = 0; ent->client->resp.score = 0; PutClientInServer(ent); } bool CTFInMatch() { if (ctfgame.match > MATCH_NONE) return true; return false; } bool CTFCheckRules() { int t; uint32_t i, j; char text[64]; edict_t *ent; if (ctfgame.election != ELECT_NONE && ctfgame.electtime <= level.time) { gi.LocBroadcast_Print(PRINT_CHAT, "Election timed out and has been cancelled.\n"); ctfgame.election = ELECT_NONE; } if (ctfgame.match != MATCH_NONE) { t = (ctfgame.matchtime - level.time).seconds(); // no team warnings in match mode ctfgame.warnactive = 0; if (t <= 0) { // time ended on something switch (ctfgame.match) { case MATCH_SETUP: // go back to normal mode if (competition->integer < 3) { ctfgame.match = MATCH_NONE; gi.cvar_set("competition", "1"); CTFResetAllPlayers(); } else { // reset the time ctfgame.matchtime = level.time + gtime_t::from_min(matchsetuptime->value); } return false; case MATCH_PREGAME: // match started! CTFStartMatch(); gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("misc/tele_up.wav"), 1, ATTN_NONE, 0); return false; case MATCH_GAME: // match ended! CTFEndMatch(); gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("misc/bigtele.wav"), 1, ATTN_NONE, 0); return false; default: break; } } if (t == ctfgame.lasttime) return false; ctfgame.lasttime = t; switch (ctfgame.match) { case MATCH_SETUP: for (j = 0, i = 1; i <= game.maxclients; i++) { ent = g_edicts + i; if (!ent->inuse) continue; if (ent->client->resp.ctf_team != CTF_NOTEAM && !ent->client->resp.ready) j++; } if (competition->integer < 3) G_FmtTo(text, "{:02}:{:02} SETUP: {} not ready", t / 60, t % 60, j); else G_FmtTo(text, "SETUP: {} not ready", j); gi.configstring(CONFIG_CTF_MATCH, text); break; case MATCH_PREGAME: G_FmtTo(text, "{:02}:{:02} UNTIL START", t / 60, t % 60); gi.configstring(CONFIG_CTF_MATCH, text); if (t <= 10 && !ctfgame.countdown) { ctfgame.countdown = true; gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("world/10_0.wav"), 1, ATTN_NONE, 0); } break; case MATCH_GAME: G_FmtTo(text, "{:02}:{:02} MATCH", t / 60, t % 60); gi.configstring(CONFIG_CTF_MATCH, text); if (t <= 10 && !ctfgame.countdown) { ctfgame.countdown = true; gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("world/10_0.wav"), 1, ATTN_NONE, 0); } break; default: break; } return false; } else { int team1 = 0, team2 = 0; if (level.time == gtime_t::from_sec(ctfgame.lasttime)) return false; ctfgame.lasttime = level.time.seconds(); // this is only done in non-match (public) mode if (warn_unbalanced->integer) { // count up the team totals for (i = 1; i <= game.maxclients; i++) { ent = g_edicts + i; if (!ent->inuse) continue; if (ent->client->resp.ctf_team == CTF_TEAM1) team1++; else if (ent->client->resp.ctf_team == CTF_TEAM2) team2++; } if (team1 - team2 >= 2 && team2 >= 2) { if (ctfgame.warnactive != CTF_TEAM1) { ctfgame.warnactive = CTF_TEAM1; gi.configstring(CONFIG_CTF_TEAMINFO, "WARNING: Red has too many players"); } } else if (team2 - team1 >= 2 && team1 >= 2) { if (ctfgame.warnactive != CTF_TEAM2) { ctfgame.warnactive = CTF_TEAM2; gi.configstring(CONFIG_CTF_TEAMINFO, "WARNING: Blue has too many players"); } } else ctfgame.warnactive = 0; } else ctfgame.warnactive = 0; } if (capturelimit->integer && (ctfgame.team1 >= capturelimit->integer || ctfgame.team2 >= capturelimit->integer)) { gi.LocBroadcast_Print(PRINT_HIGH, "$g_capturelimit_hit"); return true; } return false; } /*-------------------------------------------------------------------------- * just here to help old map conversions *--------------------------------------------------------------------------*/ TOUCH(old_teleporter_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void { edict_t *dest; vec3_t forward; if (!other->client) return; dest = G_PickTarget(self->target); if (!dest) { gi.Com_Print("Couldn't find destination\n"); return; } // ZOID CTFPlayerResetGrapple(other); // ZOID // unlink to make sure it can't possibly interfere with KillBox gi.unlinkentity(other); other->s.origin = dest->s.origin; other->s.old_origin = dest->s.origin; // other->s.origin[2] += 10; // clear the velocity and hold them in place briefly other->velocity = {}; other->client->ps.pmove.pm_time = 160; // hold time other->client->ps.pmove.pm_flags |= PMF_TIME_TELEPORT; // draw the teleport splash at source and on the player self->enemy->s.event = EV_PLAYER_TELEPORT; other->s.event = EV_PLAYER_TELEPORT; // set angles other->client->ps.pmove.delta_angles = dest->s.angles - other->client->resp.cmd_angles; other->s.angles[PITCH] = 0; other->s.angles[YAW] = dest->s.angles[YAW]; other->s.angles[ROLL] = 0; other->client->ps.viewangles = dest->s.angles; other->client->v_angle = dest->s.angles; // give a little forward velocity AngleVectors(other->client->v_angle, forward, nullptr, nullptr); other->velocity = forward * 200; gi.linkentity(other); // kill anything at the destination if (!KillBox(other, true)) { } // [Paril-KEX] move sphere, if we own it if (other->client->owned_sphere) { edict_t *sphere = other->client->owned_sphere; sphere->s.origin = other->s.origin; sphere->s.origin[2] = other->absmax[2]; sphere->s.angles[YAW] = other->s.angles[YAW]; gi.linkentity(sphere); } } /*QUAKED trigger_ctf_teleport (0.5 0.5 0.5) ? Players touching this will be teleported */ void SP_trigger_ctf_teleport(edict_t *ent) { edict_t *s; int i; if (!ent->target) { gi.Com_Print("teleporter without a target.\n"); G_FreeEdict(ent); return; } ent->svflags |= SVF_NOCLIENT; ent->solid = SOLID_TRIGGER; ent->touch = old_teleporter_touch; gi.setmodel(ent, ent->model); gi.linkentity(ent); // noise maker and splash effect dude s = G_Spawn(); ent->enemy = s; for (i = 0; i < 3; i++) s->s.origin[i] = ent->mins[i] + (ent->maxs[i] - ent->mins[i]) / 2; s->s.sound = gi.soundindex("world/hum1.wav"); gi.linkentity(s); } /*QUAKED info_ctf_teleport_destination (0.5 0.5 0.5) (-16 -16 -24) (16 16 32) Point trigger_teleports at these. */ void SP_info_ctf_teleport_destination(edict_t *ent) { ent->s.origin[2] += 16; } /*----------------------------------------------------------------------------------*/ /* ADMIN */ struct admin_settings_t { int matchlen; int matchsetuplen; int matchstartlen; bool weaponsstay; bool instantitems; bool quaddrop; bool instantweap; bool matchlock; }; void CTFAdmin_UpdateSettings(edict_t *ent, pmenuhnd_t *setmenu); void CTFOpenAdminMenu(edict_t *ent); void CTFAdmin_SettingsApply(edict_t *ent, pmenuhnd_t *p) { admin_settings_t *settings = (admin_settings_t *) p->arg; if (settings->matchlen != matchtime->value) { gi.LocBroadcast_Print(PRINT_HIGH, "{} changed the match length to {} minutes.\n", ent->client->pers.netname, settings->matchlen); if (ctfgame.match == MATCH_GAME) { // in the middle of a match, change it on the fly ctfgame.matchtime = (ctfgame.matchtime - gtime_t::from_min(matchtime->value)) + gtime_t::from_min(settings->matchlen); } ; gi.cvar_set("matchtime", G_Fmt("{}", settings->matchlen).data()); } if (settings->matchsetuplen != matchsetuptime->value) { gi.LocBroadcast_Print(PRINT_HIGH, "{} changed the match setup time to {} minutes.\n", ent->client->pers.netname, settings->matchsetuplen); if (ctfgame.match == MATCH_SETUP) { // in the middle of a match, change it on the fly ctfgame.matchtime = (ctfgame.matchtime - gtime_t::from_min(matchsetuptime->value)) + gtime_t::from_min(settings->matchsetuplen); } ; gi.cvar_set("matchsetuptime", G_Fmt("{}", settings->matchsetuplen).data()); } if (settings->matchstartlen != matchstarttime->value) { gi.LocBroadcast_Print(PRINT_HIGH, "{} changed the match start time to {} seconds.\n", ent->client->pers.netname, settings->matchstartlen); if (ctfgame.match == MATCH_PREGAME) { // in the middle of a match, change it on the fly ctfgame.matchtime = (ctfgame.matchtime - gtime_t::from_sec(matchstarttime->value)) + gtime_t::from_sec(settings->matchstartlen); } gi.cvar_set("matchstarttime", G_Fmt("{}", settings->matchstartlen).data()); } if (settings->weaponsstay != !!g_dm_weapons_stay->integer) { gi.LocBroadcast_Print(PRINT_HIGH, "{} turned {} weapons stay.\n", ent->client->pers.netname, settings->weaponsstay ? "on" : "off"); gi.cvar_set("g_dm_weapons_stay", settings->weaponsstay ? "1" : "0"); } if (settings->instantitems != !!g_dm_instant_items->integer) { gi.LocBroadcast_Print(PRINT_HIGH, "{} turned {} instant items.\n", ent->client->pers.netname, settings->instantitems ? "on" : "off"); gi.cvar_set("g_dm_instant_items", settings->instantitems ? "1" : "0"); } if (settings->quaddrop != (bool) !g_dm_no_quad_drop->integer) { gi.LocBroadcast_Print(PRINT_HIGH, "{} turned {} quad drop.\n", ent->client->pers.netname, settings->quaddrop ? "on" : "off"); gi.cvar_set("g_dm_no_quad_drop", !settings->quaddrop ? "1" : "0"); } if (settings->instantweap != !!g_instant_weapon_switch->integer) { gi.LocBroadcast_Print(PRINT_HIGH, "{} turned {} instant weapons.\n", ent->client->pers.netname, settings->instantweap ? "on" : "off"); gi.cvar_set("g_instant_weapon_switch", settings->instantweap ? "1" : "0"); } if (settings->matchlock != !!matchlock->integer) { gi.LocBroadcast_Print(PRINT_HIGH, "{} turned {} match lock.\n", ent->client->pers.netname, settings->matchlock ? "on" : "off"); gi.cvar_set("matchlock", settings->matchlock ? "1" : "0"); } PMenu_Close(ent); CTFOpenAdminMenu(ent); } void CTFAdmin_SettingsCancel(edict_t *ent, pmenuhnd_t *p) { PMenu_Close(ent); CTFOpenAdminMenu(ent); } void CTFAdmin_ChangeMatchLen(edict_t *ent, pmenuhnd_t *p) { admin_settings_t *settings = (admin_settings_t *) p->arg; settings->matchlen = (settings->matchlen % 60) + 5; if (settings->matchlen < 5) settings->matchlen = 5; CTFAdmin_UpdateSettings(ent, p); } void CTFAdmin_ChangeMatchSetupLen(edict_t *ent, pmenuhnd_t *p) { admin_settings_t *settings = (admin_settings_t *) p->arg; settings->matchsetuplen = (settings->matchsetuplen % 60) + 5; if (settings->matchsetuplen < 5) settings->matchsetuplen = 5; CTFAdmin_UpdateSettings(ent, p); } void CTFAdmin_ChangeMatchStartLen(edict_t *ent, pmenuhnd_t *p) { admin_settings_t *settings = (admin_settings_t *) p->arg; settings->matchstartlen = (settings->matchstartlen % 600) + 10; if (settings->matchstartlen < 20) settings->matchstartlen = 20; CTFAdmin_UpdateSettings(ent, p); } void CTFAdmin_ChangeWeapStay(edict_t *ent, pmenuhnd_t *p) { admin_settings_t *settings = (admin_settings_t *) p->arg; settings->weaponsstay = !settings->weaponsstay; CTFAdmin_UpdateSettings(ent, p); } void CTFAdmin_ChangeInstantItems(edict_t *ent, pmenuhnd_t *p) { admin_settings_t *settings = (admin_settings_t *) p->arg; settings->instantitems = !settings->instantitems; CTFAdmin_UpdateSettings(ent, p); } void CTFAdmin_ChangeQuadDrop(edict_t *ent, pmenuhnd_t *p) { admin_settings_t *settings = (admin_settings_t *) p->arg; settings->quaddrop = !settings->quaddrop; CTFAdmin_UpdateSettings(ent, p); } void CTFAdmin_ChangeInstantWeap(edict_t *ent, pmenuhnd_t *p) { admin_settings_t *settings = (admin_settings_t *) p->arg; settings->instantweap = !settings->instantweap; CTFAdmin_UpdateSettings(ent, p); } void CTFAdmin_ChangeMatchLock(edict_t *ent, pmenuhnd_t *p) { admin_settings_t *settings = (admin_settings_t *) p->arg; settings->matchlock = !settings->matchlock; CTFAdmin_UpdateSettings(ent, p); } void CTFAdmin_UpdateSettings(edict_t *ent, pmenuhnd_t *setmenu) { int i = 2; admin_settings_t *settings = (admin_settings_t *) setmenu->arg; PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Match Len: {:2} mins", settings->matchlen).data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchLen); i++; PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Match Setup Len: {:2} mins", settings->matchsetuplen).data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchSetupLen); i++; PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Match Start Len: {:2} secs", settings->matchstartlen).data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchStartLen); i++; PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Weapons Stay: {}", settings->weaponsstay ? "Yes" : "No").data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeWeapStay); i++; PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Instant Items: {}", settings->instantitems ? "Yes" : "No").data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeInstantItems); i++; PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Quad Drop: {}", settings->quaddrop ? "Yes" : "No").data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeQuadDrop); i++; PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Instant Weapons: {}", settings->instantweap ? "Yes" : "No").data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeInstantWeap); i++; PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Match Lock: {}", settings->matchlock ? "Yes" : "No").data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchLock); i++; PMenu_Update(ent); } const pmenu_t def_setmenu[] = { { "*Settings Menu", PMENU_ALIGN_CENTER, nullptr }, { "", PMENU_ALIGN_CENTER, nullptr }, { "", PMENU_ALIGN_LEFT, nullptr }, // int matchlen; { "", PMENU_ALIGN_LEFT, nullptr }, // int matchsetuplen; { "", PMENU_ALIGN_LEFT, nullptr }, // int matchstartlen; { "", PMENU_ALIGN_LEFT, nullptr }, // bool weaponsstay; { "", PMENU_ALIGN_LEFT, nullptr }, // bool instantitems; { "", PMENU_ALIGN_LEFT, nullptr }, // bool quaddrop; { "", PMENU_ALIGN_LEFT, nullptr }, // bool instantweap; { "", PMENU_ALIGN_LEFT, nullptr }, // bool matchlock; { "", PMENU_ALIGN_LEFT, nullptr }, { "Apply", PMENU_ALIGN_LEFT, CTFAdmin_SettingsApply }, { "Cancel", PMENU_ALIGN_LEFT, CTFAdmin_SettingsCancel } }; void CTFAdmin_Settings(edict_t *ent, pmenuhnd_t *p) { admin_settings_t *settings; pmenuhnd_t *menu; PMenu_Close(ent); settings = (admin_settings_t *) gi.TagMalloc(sizeof(*settings), TAG_LEVEL); settings->matchlen = matchtime->integer; settings->matchsetuplen = matchsetuptime->integer; settings->matchstartlen = matchstarttime->integer; settings->weaponsstay = g_dm_weapons_stay->integer; settings->instantitems = g_dm_instant_items->integer; settings->quaddrop = !g_dm_no_quad_drop->integer; settings->instantweap = g_instant_weapon_switch->integer != 0; settings->matchlock = matchlock->integer != 0; menu = PMenu_Open(ent, def_setmenu, -1, sizeof(def_setmenu) / sizeof(pmenu_t), settings, nullptr); CTFAdmin_UpdateSettings(ent, menu); } void CTFAdmin_MatchSet(edict_t *ent, pmenuhnd_t *p) { PMenu_Close(ent); if (ctfgame.match == MATCH_SETUP) { gi.LocBroadcast_Print(PRINT_CHAT, "Match has been forced to start.\n"); ctfgame.match = MATCH_PREGAME; ctfgame.matchtime = level.time + gtime_t::from_sec(matchstarttime->value); gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("misc/talk1.wav"), 1, ATTN_NONE, 0); ctfgame.countdown = false; } else if (ctfgame.match == MATCH_GAME) { gi.LocBroadcast_Print(PRINT_CHAT, "Match has been forced to terminate.\n"); ctfgame.match = MATCH_SETUP; ctfgame.matchtime = level.time + gtime_t::from_min(matchsetuptime->value); CTFResetAllPlayers(); } } void CTFAdmin_MatchMode(edict_t *ent, pmenuhnd_t *p) { PMenu_Close(ent); if (ctfgame.match != MATCH_SETUP) { if (competition->integer < 3) gi.cvar_set("competition", "2"); ctfgame.match = MATCH_SETUP; CTFResetAllPlayers(); } } void CTFAdmin_Reset(edict_t *ent, pmenuhnd_t *p) { PMenu_Close(ent); // go back to normal mode gi.LocBroadcast_Print(PRINT_CHAT, "Match mode has been terminated, reseting to normal game.\n"); ctfgame.match = MATCH_NONE; gi.cvar_set("competition", "1"); CTFResetAllPlayers(); } void CTFAdmin_Cancel(edict_t *ent, pmenuhnd_t *p) { PMenu_Close(ent); } pmenu_t adminmenu[] = { { "*Administration Menu", PMENU_ALIGN_CENTER, nullptr }, { "", PMENU_ALIGN_CENTER, nullptr }, // blank { "Settings", PMENU_ALIGN_LEFT, CTFAdmin_Settings }, { "", PMENU_ALIGN_LEFT, nullptr }, { "", PMENU_ALIGN_LEFT, nullptr }, { "Cancel", PMENU_ALIGN_LEFT, CTFAdmin_Cancel }, { "", PMENU_ALIGN_CENTER, nullptr }, }; void CTFOpenAdminMenu(edict_t *ent) { adminmenu[3].text[0] = '\0'; adminmenu[3].SelectFunc = nullptr; adminmenu[4].text[0] = '\0'; adminmenu[4].SelectFunc = nullptr; if (ctfgame.match == MATCH_SETUP) { Q_strlcpy(adminmenu[3].text, "Force start match", sizeof(adminmenu[3].text)); adminmenu[3].SelectFunc = CTFAdmin_MatchSet; Q_strlcpy(adminmenu[4].text, "Reset to pickup mode", sizeof(adminmenu[4].text)); adminmenu[4].SelectFunc = CTFAdmin_Reset; } else if (ctfgame.match == MATCH_GAME || ctfgame.match == MATCH_PREGAME) { Q_strlcpy(adminmenu[3].text, "Cancel match", sizeof(adminmenu[3].text)); adminmenu[3].SelectFunc = CTFAdmin_MatchSet; } else if (ctfgame.match == MATCH_NONE && competition->integer) { Q_strlcpy(adminmenu[3].text, "Switch to match mode", sizeof(adminmenu[3].text)); adminmenu[3].SelectFunc = CTFAdmin_MatchMode; } // if (ent->client->menu) // PMenu_Close(ent->client->menu); PMenu_Open(ent, adminmenu, -1, sizeof(adminmenu) / sizeof(pmenu_t), nullptr, nullptr); } void CTFAdmin(edict_t *ent) { if (!allow_admin->integer) { gi.LocClient_Print(ent, PRINT_HIGH, "Administration is disabled\n"); return; } if (gi.argc() > 1 && admin_password->string && *admin_password->string && !ent->client->resp.admin && strcmp(admin_password->string, gi.argv(1)) == 0) { ent->client->resp.admin = true; gi.LocBroadcast_Print(PRINT_HIGH, "{} has become an admin.\n", ent->client->pers.netname); gi.LocClient_Print(ent, PRINT_HIGH, "Type 'admin' to access the adminstration menu.\n"); } if (!ent->client->resp.admin) { CTFBeginElection(ent, ELECT_ADMIN, G_Fmt("{} has requested admin rights.\n", ent->client->pers.netname).data()); return; } if (ent->client->menu) PMenu_Close(ent); CTFOpenAdminMenu(ent); } /*----------------------------------------------------------------*/ void CTFStats(edict_t *ent) { if (!G_TeamplayEnabled()) return; ghost_t *g; static std::string text; edict_t *e2; text.clear(); if (ctfgame.match == MATCH_SETUP) { for (uint32_t i = 1; i <= game.maxclients; i++) { e2 = g_edicts + i; if (!e2->inuse) continue; if (!e2->client->resp.ready && e2->client->resp.ctf_team != CTF_NOTEAM) { std::string_view str = G_Fmt("{} is not ready.\n", e2->client->pers.netname); if (text.length() + str.length() < MAX_CTF_STAT_LENGTH - 50) text += str; } } } uint32_t i; for (i = 0, g = ctfgame.ghosts; i < MAX_CLIENTS; i++, g++) if (g->ent) break; if (i == MAX_CLIENTS) { if (!text.length()) text = "No statistics available.\n"; gi.Client_Print(ent, PRINT_HIGH, text.c_str()); return; } text += " #|Name |Score|Kills|Death|BasDf|CarDf|Effcy|\n"; for (i = 0, g = ctfgame.ghosts; i < MAX_CLIENTS; i++, g++) { if (!*g->netname) continue; int32_t e; if (g->deaths + g->kills == 0) e = 50; else e = g->kills * 100 / (g->kills + g->deaths); std::string_view str = G_Fmt("{:3}|{:<16.16}|{:5}|{:5}|{:5}|{:5}|{:5}|{:4}%|\n", g->number, g->netname, g->score, g->kills, g->deaths, g->basedef, g->carrierdef, e); if (text.length() + str.length() > MAX_CTF_STAT_LENGTH - 50) { text += "And more...\n"; break; } text += str; } gi.Client_Print(ent, PRINT_HIGH, text.c_str()); } void CTFPlayerList(edict_t *ent) { static std::string text; edict_t *e2; // number, name, connect time, ping, score, admin text.clear(); for (uint32_t i = 1; i <= game.maxclients; i++) { e2 = g_edicts + i; if (!e2->inuse) continue; std::string_view str = G_Fmt("{:3} {:<16.16} {:02}:{:02} {:4} {:3}{}{}\n", i, e2->client->pers.netname, (level.time - e2->client->resp.entertime).milliseconds() / 60000, ((level.time - e2->client->resp.entertime).milliseconds() % 60000) / 1000, e2->client->ping, e2->client->resp.score, (ctfgame.match == MATCH_SETUP || ctfgame.match == MATCH_PREGAME) ? (e2->client->resp.ready ? " (ready)" : " (notready)") : "", e2->client->resp.admin ? " (admin)" : ""); if (text.length() + str.length() > MAX_CTF_STAT_LENGTH - 50) { text += "And more...\n"; break; } text += str; } gi.Client_Print(ent, PRINT_HIGH, text.data()); } void CTFWarp(edict_t *ent) { char *token; if (gi.argc() < 2) { gi.LocClient_Print(ent, PRINT_HIGH, "Where do you want to warp to?\n"); gi.LocClient_Print(ent, PRINT_HIGH, "Available levels are: {}\n", warp_list->string); return; } const char *mlist = warp_list->string; while (*(token = COM_Parse(&mlist))) { if (Q_strcasecmp(token, gi.argv(1)) == 0) break; } if (!*token) { gi.LocClient_Print(ent, PRINT_HIGH, "Unknown CTF level.\n"); gi.LocClient_Print(ent, PRINT_HIGH, "Available levels are: {}\n", warp_list->string); return; } if (ent->client->resp.admin) { gi.LocBroadcast_Print(PRINT_HIGH, "{} is warping to level {}.\n", ent->client->pers.netname, gi.argv(1)); Q_strlcpy(level.forcemap, gi.argv(1), sizeof(level.forcemap)); EndDMLevel(); return; } if (CTFBeginElection(ent, ELECT_MAP, G_Fmt("{} has requested warping to level {}.\n", ent->client->pers.netname, gi.argv(1)).data())) Q_strlcpy(ctfgame.elevel, gi.argv(1), sizeof(ctfgame.elevel)); } void CTFBoot(edict_t *ent) { edict_t *targ; if (!ent->client->resp.admin) { gi.LocClient_Print(ent, PRINT_HIGH, "You are not an admin.\n"); return; } if (gi.argc() < 2) { gi.LocClient_Print(ent, PRINT_HIGH, "Who do you want to kick?\n"); return; } if (*gi.argv(1) < '0' && *gi.argv(1) > '9') { gi.LocClient_Print(ent, PRINT_HIGH, "Specify the player number to kick.\n"); return; } uint32_t i = strtoul(gi.argv(1), nullptr, 10); if (i < 1 || i > game.maxclients) { gi.LocClient_Print(ent, PRINT_HIGH, "Invalid player number.\n"); return; } targ = g_edicts + i; if (!targ->inuse) { gi.LocClient_Print(ent, PRINT_HIGH, "That player number is not connected.\n"); return; } gi.AddCommandString(G_Fmt("kick {}\n", i - 1).data()); } void CTFSetPowerUpEffect(edict_t *ent, effects_t def) { if (ent->client->resp.ctf_team == CTF_TEAM1 && def == EF_QUAD) ent->s.effects |= EF_PENT; // red else if (ent->client->resp.ctf_team == CTF_TEAM2 && def == EF_PENT) ent->s.effects |= EF_QUAD; // blue else ent->s.effects |= def; }