// Copyright (c) ZeniMax Media Inc. // Licensed under the GNU General Public License 2.0. #include "g_local.h" #include "m_player.h" void SelectNextItem(edict_t *ent, item_flags_t itflags, bool menu = true) { gclient_t *cl; item_id_t i, index; gitem_t *it; cl = ent->client; // ZOID if (menu && cl->menu) { PMenu_Next(ent); return; } else if (menu && cl->chase_target) { ChaseNext(ent); return; } // ZOID // scan for the next valid one for (i = static_cast(IT_NULL + 1); i <= IT_TOTAL; i = static_cast(i + 1)) { index = static_cast((cl->pers.selected_item + i) % IT_TOTAL); if (!cl->pers.inventory[index]) continue; it = &itemlist[index]; if (!it->use) continue; if (!(it->flags & itflags)) continue; cl->pers.selected_item = index; cl->pers.selected_item_time = level.time + SELECTED_ITEM_TIME; cl->ps.stats[STAT_SELECTED_ITEM_NAME] = CS_ITEMS + index; return; } cl->pers.selected_item = IT_NULL; } void SelectPrevItem(edict_t *ent, item_flags_t itflags) { gclient_t *cl; item_id_t i, index; gitem_t *it; cl = ent->client; // ZOID if (cl->menu) { PMenu_Prev(ent); return; } else if (cl->chase_target) { ChasePrev(ent); return; } // ZOID // scan for the next valid one for (i = static_cast(IT_NULL + 1); i <= IT_TOTAL; i = static_cast(i + 1)) { index = static_cast((cl->pers.selected_item + IT_TOTAL - i) % IT_TOTAL); if (!cl->pers.inventory[index]) continue; it = &itemlist[index]; if (!it->use) continue; if (!(it->flags & itflags)) continue; cl->pers.selected_item = index; cl->pers.selected_item_time = level.time + SELECTED_ITEM_TIME; cl->ps.stats[STAT_SELECTED_ITEM_NAME] = CS_ITEMS + index; return; } cl->pers.selected_item = IT_NULL; } void ValidateSelectedItem(edict_t *ent) { gclient_t *cl; cl = ent->client; if (cl->pers.inventory[cl->pers.selected_item]) return; // valid SelectNextItem(ent, IF_ANY, false); } //================================================================================= inline bool G_CheatCheck(edict_t *ent) { if (game.maxclients > 1 && !sv_cheats->integer) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_need_cheats"); return false; } return true; } static void SpawnAndGiveItem(edict_t *ent, item_id_t id) { gitem_t *it = GetItemByIndex(id); if (!it) return; edict_t *it_ent = G_Spawn(); it_ent->classname = it->classname; SpawnItem(it_ent, it); if (it_ent->inuse) { Touch_Item(it_ent, ent, null_trace, true); if (it_ent->inuse) G_FreeEdict(it_ent); } } /* ================== Cmd_Give_f Give items to a client ================== */ void Cmd_Give_f(edict_t *ent) { const char *name; gitem_t *it; item_id_t index; int i; bool give_all; edict_t *it_ent; if (!G_CheatCheck(ent)) return; name = gi.args(); if (Q_strcasecmp(name, "all") == 0) give_all = true; else give_all = false; if (give_all || Q_strcasecmp(gi.argv(1), "health") == 0) { if (gi.argc() == 3) ent->health = atoi(gi.argv(2)); else ent->health = ent->max_health; if (!give_all) return; } if (give_all || Q_strcasecmp(name, "weapons") == 0) { for (i = 0; i < IT_TOTAL; i++) { it = itemlist + i; if (!it->pickup) continue; if (!(it->flags & IF_WEAPON)) continue; ent->client->pers.inventory[i] += 1; } if (!give_all) return; } if (give_all || Q_strcasecmp(name, "ammo") == 0) { if (give_all) SpawnAndGiveItem(ent, IT_ITEM_PACK); for (i = 0; i < IT_TOTAL; i++) { it = itemlist + i; if (!it->pickup) continue; if (!(it->flags & IF_AMMO)) continue; Add_Ammo(ent, it, 1000); } if (!give_all) return; } if (give_all || Q_strcasecmp(name, "armor") == 0) { ent->client->pers.inventory[IT_ARMOR_JACKET] = 0; ent->client->pers.inventory[IT_ARMOR_COMBAT] = 0; ent->client->pers.inventory[IT_ARMOR_BODY] = GetItemByIndex(IT_ARMOR_BODY)->armor_info->max_count; if (!give_all) return; } if (give_all) { SpawnAndGiveItem(ent, IT_ITEM_POWER_SHIELD); if (!give_all) return; } if (give_all) { for (i = 0; i < IT_TOTAL; i++) { it = itemlist + i; if (!it->pickup) continue; // ROGUE if (it->flags & (IF_ARMOR | IF_WEAPON | IF_AMMO | IF_NOT_GIVEABLE | IF_TECH)) continue; else if (it->pickup == CTFPickup_Flag) continue; else if ((it->flags & IF_HEALTH) && !it->use) continue; // ROGUE ent->client->pers.inventory[i] = (it->flags & IF_KEY) ? 8 : 1; } G_CheckPowerArmor(ent); ent->client->pers.power_cubes = 0xFF; return; } it = FindItem(name); if (!it) { name = gi.argv(1); it = FindItem(name); } if (!it) it = FindItemByClassname(name); if (!it) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_unknown_item"); return; } // ROGUE if (it->flags & IF_NOT_GIVEABLE) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_not_giveable"); return; } // ROGUE index = it->id; if (!it->pickup) { ent->client->pers.inventory[index] = 1; return; } if (it->flags & IF_AMMO) { if (gi.argc() == 3) ent->client->pers.inventory[index] = atoi(gi.argv(2)); else ent->client->pers.inventory[index] += it->quantity; } else { it_ent = G_Spawn(); it_ent->classname = it->classname; SpawnItem(it_ent, it); // PMM - since some items don't actually spawn when you say to .. if (!it_ent->inuse) return; // pmm Touch_Item(it_ent, ent, null_trace, true); if (it_ent->inuse) G_FreeEdict(it_ent); } } void Cmd_SetPOI_f(edict_t *self) { if (!G_CheatCheck(self)) return; level.current_poi = self->s.origin; level.valid_poi = true; } void Cmd_CheckPOI_f(edict_t *self) { if (!G_CheatCheck(self)) return; if (!level.valid_poi) return; char visible_pvs = gi.inPVS(self->s.origin, level.current_poi, false) ? 'y' : 'n'; char visible_pvs_portals = gi.inPVS(self->s.origin, level.current_poi, true) ? 'y' : 'n'; char visible_phs = gi.inPHS(self->s.origin, level.current_poi, false) ? 'y' : 'n'; char visible_phs_portals = gi.inPHS(self->s.origin, level.current_poi, true) ? 'y' : 'n'; gi.Com_PrintFmt("pvs {} + portals {}, phs {} + portals {}\n", visible_pvs, visible_pvs_portals, visible_phs, visible_phs_portals); } // [Paril-KEX] static void Cmd_Target_f(edict_t *ent) { if (!G_CheatCheck(ent)) return; ent->target = gi.argv(1); G_UseTargets(ent, ent); ent->target = nullptr; } /* ================== Cmd_God_f Sets client to godmode argv(0) god ================== */ void Cmd_God_f(edict_t *ent) { const char *msg; if (!G_CheatCheck(ent)) return; ent->flags ^= FL_GODMODE; if (!(ent->flags & FL_GODMODE)) msg = "godmode OFF\n"; else msg = "godmode ON\n"; gi.LocClient_Print(ent, PRINT_HIGH, msg); } void ED_ParseField(const char *key, const char *value, edict_t *ent); /* ================== Cmd_Immortal_f Sets client to immortal - take damage but never go below 1 hp argv(0) immortal ================== */ void Cmd_Immortal_f(edict_t *ent) { const char *msg; if (!G_CheatCheck(ent)) return; ent->flags ^= FL_IMMORTAL; if (!(ent->flags & FL_IMMORTAL)) msg = "immortal OFF\n"; else msg = "immortal ON\n"; gi.LocClient_Print(ent, PRINT_HIGH, msg); } /* ================= Cmd_Spawn_f Spawn class name argv(0) spawn argv(1) argv(2+n) "key"... argv(3+n) "value"... ================= */ void Cmd_Spawn_f(edict_t *ent) { if (!G_CheatCheck(ent)) return; solid_t backup = ent->solid; ent->solid = SOLID_NOT; gi.linkentity(ent); edict_t* other = G_Spawn(); other->classname = gi.argv(1); other->s.origin = ent->s.origin + (AngleVectors(ent->s.angles).forward * 24.f); other->s.angles[1] = ent->s.angles[1]; st = {}; if (gi.argc() > 3) { for (int i = 2; i < gi.argc(); i += 2) ED_ParseField(gi.argv(i), gi.argv(i + 1), other); } ED_CallSpawn(other); if (other->inuse) { vec3_t forward, end; AngleVectors(ent->client->v_angle, forward, nullptr, nullptr); end = ent->s.origin; end[2] += ent->viewheight; end += (forward * 8192); trace_t tr = gi.traceline(ent->s.origin + vec3_t{0.f, 0.f, (float) ent->viewheight}, end, other, MASK_SHOT | CONTENTS_MONSTERCLIP); other->s.origin = tr.endpos; for (int32_t i = 0; i < 3; i++) { if (tr.plane.normal[i] > 0) other->s.origin[i] -= other->mins[i] * tr.plane.normal[i]; else other->s.origin[i] += other->maxs[i] * -tr.plane.normal[i]; } while (gi.trace(other->s.origin, other->mins, other->maxs, other->s.origin, other, MASK_SHOT | CONTENTS_MONSTERCLIP) .startsolid) { float dx = other->mins[0] - other->maxs[0]; float dy = other->mins[1] - other->maxs[1]; other->s.origin += forward * -sqrtf(dx * dx + dy * dy); if ((other->s.origin - ent->s.origin).dot(forward) < 0) { gi.Client_Print(ent, PRINT_HIGH, "Couldn't find a suitable spawn location\n"); G_FreeEdict(other); break; } } if (other->inuse) gi.linkentity(other); if ((other->svflags & SVF_MONSTER) && other->think) other->think(other); } ent->solid = backup; gi.linkentity(ent); } /* ================= Cmd_Spawn_f Telepo' argv(0) teleport argv(1) x argv(2) y argv(3) z ================= */ void Cmd_Teleport_f(edict_t *ent) { if (!G_CheatCheck(ent)) return; if (gi.argc() < 4) { gi.LocClient_Print(ent, PRINT_HIGH, "Not enough args; teleport x y z\n"); return; } ent->s.origin[0] = (float) atof(gi.argv(1)); ent->s.origin[1] = (float) atof(gi.argv(2)); ent->s.origin[2] = (float) atof(gi.argv(3)); if (gi.argc() >= 4) { float pitch = (float) atof(gi.argv(4)); float yaw = (float) atof(gi.argv(5)); float roll = (float) atof(gi.argv(6)); vec3_t ang { pitch, yaw, roll }; ent->client->ps.pmove.delta_angles = ( ang - ent->client->resp.cmd_angles ); ent->client->ps.viewangles = {}; ent->client->v_angle = {}; } gi.linkentity(ent); } /* ================== Cmd_Notarget_f Sets client to notarget argv(0) notarget ================== */ void Cmd_Notarget_f(edict_t *ent) { const char *msg; if (!G_CheatCheck(ent)) return; ent->flags ^= FL_NOTARGET; if (!(ent->flags & FL_NOTARGET)) msg = "notarget OFF\n"; else msg = "notarget ON\n"; gi.LocClient_Print(ent, PRINT_HIGH, msg); } /* ================== Cmd_Novisible_f Sets client to "super notarget" argv(0) notarget ================== */ void Cmd_Novisible_f(edict_t *ent) { const char *msg; if (!G_CheatCheck(ent)) return; ent->flags ^= FL_NOVISIBLE; if (!(ent->flags & FL_NOVISIBLE)) msg = "novisible OFF\n"; else msg = "novisible ON\n"; gi.LocClient_Print(ent, PRINT_HIGH, msg); } void Cmd_AlertAll_f(edict_t *ent) { if (!G_CheatCheck(ent)) return; for (size_t i = 0; i < globals.num_edicts; i++) { edict_t *t = &g_edicts[i]; if (!t->inuse || t->health <= 0 || !(t->svflags & SVF_MONSTER)) continue; t->enemy = ent; FoundTarget(t); } } /* ================== Cmd_Noclip_f argv(0) noclip ================== */ void Cmd_Noclip_f(edict_t *ent) { const char *msg; if (!G_CheatCheck(ent)) return; if (ent->movetype == MOVETYPE_NOCLIP) { ent->movetype = MOVETYPE_WALK; msg = "noclip OFF\n"; } else { ent->movetype = MOVETYPE_NOCLIP; msg = "noclip ON\n"; } gi.LocClient_Print(ent, PRINT_HIGH, msg); } /* ================== Cmd_Use_f Use an inventory item ================== */ void Cmd_Use_f(edict_t *ent) { item_id_t index; gitem_t *it; const char *s; if (ent->health <= 0 || ent->deadflag) return; s = gi.args(); const char *cmd = gi.argv(0); if (!Q_strcasecmp(cmd, "use_index") || !Q_strcasecmp(cmd, "use_index_only")) { it = GetItemByIndex((item_id_t) atoi(s)); } else { it = FindItem(s); } if (!it) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_unknown_item_name", s); return; } if (!it->use) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_item_not_usable"); return; } index = it->id; // Paril: Use_Weapon handles weapon availability if (!(it->flags & IF_WEAPON) && !ent->client->pers.inventory[index]) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_out_of_item", it->pickup_name); return; } // allow weapon chains for use ent->client->no_weapon_chains = !!strcmp(gi.argv(0), "use") && !!strcmp(gi.argv(0), "use_index"); it->use(ent, it); ValidateSelectedItem(ent); } /* ================== Cmd_Drop_f Drop an inventory item ================== */ void Cmd_Drop_f(edict_t *ent) { item_id_t index; gitem_t *it; const char *s; if (ent->health <= 0 || ent->deadflag) return; // ZOID--special case for tech powerups if (Q_strcasecmp(gi.args(), "tech") == 0) { it = CTFWhat_Tech(ent); if (it) { it->drop(ent, it); ValidateSelectedItem(ent); } return; } // ZOID s = gi.args(); const char *cmd = gi.argv(0); if (!Q_strcasecmp(cmd, "drop_index")) { it = GetItemByIndex((item_id_t) atoi(s)); } else { it = FindItem(s); } if (!it) { gi.LocClient_Print(ent, PRINT_HIGH, "Unknown item : {}\n", s); return; } if (!it->drop) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_item_not_droppable"); return; } index = it->id; if (!ent->client->pers.inventory[index]) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_out_of_item", it->pickup_name); return; } it->drop(ent, it); ValidateSelectedItem(ent); } /* ================= Cmd_Inven_f ================= */ void Cmd_Inven_f(edict_t *ent) { int i; gclient_t *cl; cl = ent->client; cl->showscores = false; cl->showhelp = false; globals.server_flags &= ~SERVER_FLAG_SLOW_TIME; // ZOID if (ent->client->menu) { PMenu_Close(ent); ent->client->update_chase = true; return; } // ZOID if (cl->showinventory) { cl->showinventory = false; return; } // ZOID if (G_TeamplayEnabled() && cl->resp.ctf_team == CTF_NOTEAM) { CTFOpenJoinMenu(ent); return; } // ZOID cl->showinventory = true; gi.WriteByte(svc_inventory); for (i = 0; i < IT_TOTAL; i++) gi.WriteShort(cl->pers.inventory[i]); for (; i < MAX_ITEMS; i++) gi.WriteShort(0); gi.unicast(ent, true); } /* ================= Cmd_InvUse_f ================= */ void Cmd_InvUse_f(edict_t *ent) { gitem_t *it; // ZOID if (ent->client->menu) { PMenu_Select(ent); return; } // ZOID if (ent->health <= 0 || ent->deadflag) return; ValidateSelectedItem(ent); if (ent->client->pers.selected_item == IT_NULL) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_no_item_to_use"); return; } it = &itemlist[ent->client->pers.selected_item]; if (!it->use) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_item_not_usable"); return; } // don't allow weapon chains for invuse ent->client->no_weapon_chains = true; it->use(ent, it); ValidateSelectedItem(ent); } /* ================= Cmd_WeapPrev_f ================= */ void Cmd_WeapPrev_f(edict_t *ent) { gclient_t *cl; item_id_t i, index; gitem_t *it; item_id_t selected_weapon; cl = ent->client; if (ent->health <= 0 || ent->deadflag) return; if (!cl->pers.weapon) return; // don't allow weapon chains for weapprev cl->no_weapon_chains = true; selected_weapon = cl->pers.weapon->id; // scan for the next valid one for (i = static_cast(IT_NULL + 1); i <= IT_TOTAL; i = static_cast(i + 1)) { // PMM - prevent scrolling through ALL weapons index = static_cast((selected_weapon + IT_TOTAL - i) % IT_TOTAL); if (!cl->pers.inventory[index]) continue; it = &itemlist[index]; if (!it->use) continue; if (!(it->flags & IF_WEAPON)) continue; it->use(ent, it); // ROGUE if (cl->newweapon == it) return; // successful // ROGUE } } /* ================= Cmd_WeapNext_f ================= */ void Cmd_WeapNext_f(edict_t *ent) { gclient_t *cl; item_id_t i, index; gitem_t *it; item_id_t selected_weapon; cl = ent->client; if (ent->health <= 0 || ent->deadflag) return; if (!cl->pers.weapon) return; // don't allow weapon chains for weapnext cl->no_weapon_chains = true; selected_weapon = cl->pers.weapon->id; // scan for the next valid one for (i = static_cast(IT_NULL + 1); i <= IT_TOTAL; i = static_cast(i + 1)) { // PMM - prevent scrolling through ALL weapons index = static_cast((selected_weapon + i) % IT_TOTAL); if (!cl->pers.inventory[index]) continue; it = &itemlist[index]; if (!it->use) continue; if (!(it->flags & IF_WEAPON)) continue; it->use(ent, it); // PMM - prevent scrolling through ALL weapons // ROGUE if (cl->newweapon == it) return; // ROGUE } } /* ================= Cmd_WeapLast_f ================= */ void Cmd_WeapLast_f(edict_t *ent) { gclient_t *cl; int index; gitem_t *it; cl = ent->client; if (ent->health <= 0 || ent->deadflag) return; if (!cl->pers.weapon || !cl->pers.lastweapon) return; // don't allow weapon chains for weaplast cl->no_weapon_chains = true; index = cl->pers.lastweapon->id; if (!cl->pers.inventory[index]) return; it = &itemlist[index]; if (!it->use) return; if (!(it->flags & IF_WEAPON)) return; it->use(ent, it); } /* ================= Cmd_InvDrop_f ================= */ void Cmd_InvDrop_f(edict_t *ent) { gitem_t *it; if (ent->health <= 0 || ent->deadflag) return; ValidateSelectedItem(ent); if (ent->client->pers.selected_item == IT_NULL) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_no_item_to_drop"); return; } it = &itemlist[ent->client->pers.selected_item]; if (!it->drop) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_item_not_droppable"); return; } it->drop(ent, it); ValidateSelectedItem(ent); } /* ================= Cmd_Kill_f ================= */ void Cmd_Kill_f(edict_t *ent) { // ZOID if (ent->client->resp.spectator) return; // ZOID if ((level.time - ent->client->respawn_time) < 5_sec) return; ent->flags &= ~FL_GODMODE; ent->health = 0; // ROGUE // make sure no trackers are still hurting us. if (ent->client->tracker_pain_time) RemoveAttackingPainDaemons(ent); if (ent->client->owned_sphere) { G_FreeEdict(ent->client->owned_sphere); ent->client->owned_sphere = nullptr; } // ROGUE // [Paril-KEX] don't allow kill to take points away in TDM player_die(ent, ent, ent, 100000, vec3_origin, { MOD_SUICIDE, !!teamplay->integer }); } /* ================= Cmd_Kill_AI_f ================= */ void Cmd_Kill_AI_f( edict_t * ent ) { if ( !sv_cheats->integer ) { gi.LocClient_Print( ent, PRINT_HIGH, "Kill_AI: Cheats Must Be Enabled!\n" ); return; } // except the one we're looking at... edict_t *looked_at = nullptr; vec3_t start = ent->s.origin + vec3_t{0.f, 0.f, (float) ent->viewheight}; vec3_t end = start + ent->client->v_forward * 1024.f; looked_at = gi.traceline(start, end, ent, MASK_SHOT).ent; const int numEdicts = globals.num_edicts; for ( int edictIdx = 1; edictIdx < numEdicts; ++edictIdx ) { edict_t * edict = &g_edicts[ edictIdx ]; if ( !edict->inuse || edict == looked_at ) { continue; } if ( ( edict->svflags & SVF_MONSTER ) == 0 ) { continue; } G_FreeEdict( edict ); } gi.LocClient_Print( ent, PRINT_HIGH, "Kill_AI: All AI Are Dead...\n" ); } /* ================= Cmd_Where_f ================= */ void Cmd_Where_f( edict_t * ent ) { if ( ent == nullptr || ent->client == nullptr ) { return; } const vec3_t & origin = ent->s.origin; std::string location; fmt::format_to( std::back_inserter( location ), FMT_STRING( "{:.1f} {:.1f} {:.1f} {:.1f} {:.1f} {:.1f}\n" ), origin[ 0 ], origin[ 1 ], origin[ 2 ], ent->client->ps.viewangles[0], ent->client->ps.viewangles[1], ent->client->ps.viewangles[2] ); gi.LocClient_Print( ent, PRINT_HIGH, "Location: {}\n", location.c_str() ); gi.SendToClipBoard( location.c_str() ); } /* ================= Cmd_Clear_AI_Enemy_f ================= */ void Cmd_Clear_AI_Enemy_f( edict_t * ent ) { if ( !sv_cheats->integer ) { gi.LocClient_Print( ent, PRINT_HIGH, "Cmd_Clear_AI_Enemy: Cheats Must Be Enabled!\n" ); return; } const int numEdicts = globals.num_edicts; for ( int edictIdx = 1; edictIdx < numEdicts; ++edictIdx ) { edict_t * edict = &g_edicts[ edictIdx ]; if ( !edict->inuse ) { continue; } if ( ( edict->svflags & SVF_MONSTER ) == 0 ) { continue; } edict->monsterinfo.aiflags |= AI_FORGET_ENEMY; } gi.LocClient_Print( ent, PRINT_HIGH, "Cmd_Clear_AI_Enemy: Clear All AI Enemies...\n" ); } /* ================= Cmd_PutAway_f ================= */ void Cmd_PutAway_f(edict_t *ent) { ent->client->showscores = false; ent->client->showhelp = false; ent->client->showinventory = false; globals.server_flags &= ~SERVER_FLAG_SLOW_TIME; // ZOID if (ent->client->menu) PMenu_Close(ent); ent->client->update_chase = true; // ZOID } int PlayerSort(const void *a, const void *b) { int anum, bnum; anum = *(const int *) a; bnum = *(const int *) b; anum = game.clients[anum].ps.stats[STAT_FRAGS]; bnum = game.clients[bnum].ps.stats[STAT_FRAGS]; if (anum < bnum) return -1; if (anum > bnum) return 1; return 0; } constexpr size_t MAX_IDEAL_PACKET_SIZE = 1024; /* ================= Cmd_Players_f ================= */ void Cmd_Players_f(edict_t *ent) { size_t i; size_t count; static std::string small, large; int index[MAX_CLIENTS]; small.clear(); large.clear(); count = 0; for (i = 0; i < game.maxclients; i++) if (game.clients[i].pers.connected) { index[count] = i; count++; } // sort by frags qsort(index, count, sizeof(index[0]), PlayerSort); // print information large[0] = 0; if (count) { for (i = 0; i < count; i++) { fmt::format_to(std::back_inserter(small), FMT_STRING("{:3} {}\n"), game.clients[index[i]].ps.stats[STAT_FRAGS], game.clients[index[i]].pers.netname); if (small.length() + large.length() > MAX_IDEAL_PACKET_SIZE - 50) { // can't print all of them in one packet large += "...\n"; break; } large += small; small.clear(); } // remove the last newline large.pop_back(); } gi.LocClient_Print(ent, PRINT_HIGH | PRINT_NO_NOTIFY, "$g_players", large.c_str(), count); } bool CheckFlood(edict_t *ent) { int i; gclient_t *cl; if (flood_msgs->integer) { cl = ent->client; if (level.time < cl->flood_locktill) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_flood_cant_talk", (cl->flood_locktill - level.time).seconds()); return true; } i = cl->flood_whenhead - flood_msgs->integer + 1; if (i < 0) i = (sizeof(cl->flood_when) / sizeof(cl->flood_when[0])) + i; if (i >= q_countof(cl->flood_when)) i = 0; if (cl->flood_when[i] && level.time - cl->flood_when[i] < gtime_t::from_sec(flood_persecond->value)) { cl->flood_locktill = level.time + gtime_t::from_sec(flood_waitdelay->value); gi.LocClient_Print(ent, PRINT_CHAT, "$g_flood_cant_talk", flood_waitdelay->integer); return true; } cl->flood_whenhead = (cl->flood_whenhead + 1) % (sizeof(cl->flood_when) / sizeof(cl->flood_when[0])); cl->flood_when[cl->flood_whenhead] = level.time; } return false; } /* ================= Cmd_Wave_f ================= */ void Cmd_Wave_f(edict_t *ent) { int i; i = atoi(gi.argv(1)); // no dead or noclip waving if (ent->deadflag || ent->movetype == MOVETYPE_NOCLIP) return; // can't wave when ducked bool do_animate = ent->client->anim_priority <= ANIM_WAVE && !(ent->client->ps.pmove.pm_flags & PMF_DUCKED); if (do_animate) ent->client->anim_priority = ANIM_WAVE; const char *other_notify_msg = nullptr, *other_notify_none_msg = nullptr; vec3_t start, dir; P_ProjectSource(ent, ent->client->v_angle, { 0, 0, 0 }, start, dir); // see who we're aiming at edict_t *aiming_at = nullptr; float best_dist = -9999; for (auto player : active_players()) { if (player == ent) continue; vec3_t cdir = player->s.origin - start; float dist = cdir.normalize(); float dot = ent->client->v_forward.dot(cdir); if (dot < 0.97) continue; else if (dist < best_dist) continue; best_dist = dist; aiming_at = player; } switch (i) { case GESTURE_FLIP_OFF: other_notify_msg = "$g_flipoff_other"; other_notify_none_msg = "$g_flipoff_none"; if (do_animate) { ent->s.frame = FRAME_flip01 - 1; ent->client->anim_end = FRAME_flip12; } break; case GESTURE_SALUTE: other_notify_msg = "$g_salute_other"; other_notify_none_msg = "$g_salute_none"; if (do_animate) { ent->s.frame = FRAME_salute01 - 1; ent->client->anim_end = FRAME_salute11; } break; case GESTURE_TAUNT: other_notify_msg = "$g_taunt_other"; other_notify_none_msg = "$g_taunt_none"; if (do_animate) { ent->s.frame = FRAME_taunt01 - 1; ent->client->anim_end = FRAME_taunt17; } break; case GESTURE_WAVE: other_notify_msg = "$g_wave_other"; other_notify_none_msg = "$g_wave_none"; if (do_animate) { ent->s.frame = FRAME_wave01 - 1; ent->client->anim_end = FRAME_wave11; } break; case GESTURE_POINT: default: other_notify_msg = "$g_point_other"; other_notify_none_msg = "$g_point_none"; if (do_animate) { ent->s.frame = FRAME_point01 - 1; ent->client->anim_end = FRAME_point12; } break; } bool has_a_target = false; if (i == GESTURE_POINT) { for (auto player : active_players()) { if (player == ent) continue; else if (!OnSameTeam(ent, player)) continue; has_a_target = true; break; } } if (i == GESTURE_POINT && has_a_target) { // don't do this stuff if we're flooding if (CheckFlood(ent)) return; trace_t tr = gi.traceline(start, start + (ent->client->v_forward * 2048), ent, MASK_SHOT & ~CONTENTS_WINDOW); other_notify_msg = "$g_point_other_ping"; uint32_t key = GetUnicastKey(); if (tr.fraction != 1.0f) { // send to all teammates for (auto player : active_players()) { if (player != ent && !OnSameTeam(ent, player)) continue; gi.WriteByte(svc_poi); gi.WriteShort(POI_PING + (ent->s.number - 1)); gi.WriteShort(5000); gi.WritePosition(tr.endpos); gi.WriteShort(level.pic_ping); gi.WriteByte(208); gi.WriteByte(POI_FLAG_NONE); gi.unicast(player, false); gi.local_sound(player, CHAN_AUTO, gi.soundindex("misc/help_marker.wav"), 1.0f, ATTN_NONE, 0.0f, key); gi.LocClient_Print(player, PRINT_HIGH, other_notify_msg, ent->client->pers.netname); } } } else { if (CheckFlood(ent)) return; edict_t* targ = nullptr; while ((targ = findradius(targ, ent->s.origin, 1024)) != nullptr) { if (ent == targ) continue; if (!targ->client) continue; if (!gi.inPVS(ent->s.origin, targ->s.origin, false)) continue; if (aiming_at && other_notify_msg) gi.LocClient_Print(targ, PRINT_TTS, other_notify_msg, ent->client->pers.netname, aiming_at->client->pers.netname); else if (other_notify_none_msg) gi.LocClient_Print(targ, PRINT_TTS, other_notify_none_msg, ent->client->pers.netname); } if (aiming_at && other_notify_msg) gi.LocClient_Print(ent, PRINT_TTS, other_notify_msg, ent->client->pers.netname, aiming_at->client->pers.netname); else if (other_notify_none_msg) gi.LocClient_Print(ent, PRINT_TTS, other_notify_none_msg, ent->client->pers.netname); } ent->client->anim_time = 0_ms; } #ifndef KEX_Q2_GAME /* ================== Cmd_Say_f NB: only used for non-Playfab stuff ================== */ void Cmd_Say_f(edict_t *ent, bool arg0) { edict_t *other; const char *p_in; static std::string text; if (gi.argc() < 2 && !arg0) return; else if (CheckFlood(ent)) return; text.clear(); fmt::format_to(std::back_inserter(text), FMT_STRING("{}: "), ent->client->pers.netname); if (arg0) { text += gi.argv(0); text += " "; text += gi.args(); } else { p_in = gi.args(); size_t in_len = strlen(p_in); if (p_in[0] == '\"' && p_in[in_len - 1] == '\"') text += std::string_view(p_in + 1, in_len - 2); else text += p_in; } // don't let text be too long for malicious reasons if (text.length() > 150) text.resize(150); if (text.back() != '\n') text.push_back('\n'); if (sv_dedicated->integer) gi.Client_Print(nullptr, PRINT_CHAT, text.c_str()); for (uint32_t j = 1; j <= game.maxclients; j++) { other = &g_edicts[j]; if (!other->inuse) continue; if (!other->client) continue; gi.Client_Print(other, PRINT_CHAT, text.c_str()); } } #endif void Cmd_PlayerList_f(edict_t *ent) { uint32_t i; static std::string str, text; edict_t *e2; str.clear(); text.clear(); // connect time, ping, score, name for (i = 0, e2 = g_edicts + 1; i < game.maxclients; i++, e2++) { if (!e2->inuse) continue; fmt::format_to(std::back_inserter(str), FMT_STRING("{:02}:{:02} {:4} {:3} {}{}\n"), (level.time - e2->client->resp.entertime).milliseconds() / 60000, ((level.time - e2->client->resp.entertime).milliseconds() % 60000) / 1000, e2->client->ping, e2->client->resp.score, e2->client->pers.netname, e2->client->resp.spectator ? " (spectator)" : ""); if (text.length() + str.length() > MAX_IDEAL_PACKET_SIZE - 50) { text += "...\n"; break; } text += str; } if (text.length()) gi.Client_Print(ent, PRINT_HIGH, text.c_str()); } void Cmd_Switchteam_f(edict_t* ent) { if (!G_TeamplayEnabled()) return; // [Paril-KEX] in force-join, just do a regular team join. if (g_teamplay_force_join->integer) { // check if we should even switch teams edict_t *player; uint32_t team1count = 0, team2count = 0; ctfteam_t best_team; for (uint32_t i = 1; i <= game.maxclients; i++) { player = &g_edicts[i]; // NB: we are counting ourselves in this one, unlike // the other assign team func if (!player->inuse) continue; switch (player->client->resp.ctf_team) { case CTF_TEAM1: team1count++; break; case CTF_TEAM2: team2count++; break; default: break; } } if (team1count < team2count) best_team = CTF_TEAM1; else best_team = CTF_TEAM2; if (ent->client->resp.ctf_team != best_team) { //// ent->svflags = SVF_NONE; ent->flags &= ~FL_GODMODE; ent->client->resp.ctf_team = best_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(best_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(best_team)); } return; } if (ent->client->resp.ctf_team != CTF_NOTEAM) CTFObserver(ent); if (!ent->client->menu) CTFOpenJoinMenu(ent); } static void Cmd_ListMonsters_f(edict_t *ent) { if (!G_CheatCheck(ent)) return; else if (!g_debug_monster_kills->integer) return; for (size_t i = 0; i < level.total_monsters; i++) { edict_t *e = level.monsters_registered[i]; if (!e || !e->inuse) continue; else if (!(e->svflags & SVF_MONSTER) || (e->monsterinfo.aiflags & AI_DO_NOT_COUNT)) continue; else if (e->deadflag) continue; gi.Com_PrintFmt("{}\n", *e); } } /* ================= ClientCommand ================= */ void ClientCommand(edict_t *ent) { const char *cmd; if (!ent->client) return; // not fully in game yet cmd = gi.argv(0); if (Q_strcasecmp(cmd, "players") == 0) { Cmd_Players_f(ent); return; } // [Paril-KEX] these have to go through the lobby system #ifndef KEX_Q2_GAME if (Q_strcasecmp(cmd, "say") == 0) { Cmd_Say_f(ent, false); return; } if (Q_strcasecmp(cmd, "say_team") == 0 || Q_strcasecmp(cmd, "steam") == 0) { if (G_TeamplayEnabled()) CTFSay_Team(ent, gi.args()); else Cmd_Say_f(ent, false); return; } #endif if (Q_strcasecmp(cmd, "score") == 0) { Cmd_Score_f(ent); return; } if (Q_strcasecmp(cmd, "help") == 0) { Cmd_Help_f(ent); return; } if (Q_strcasecmp(cmd, "listmonsters") == 0) { Cmd_ListMonsters_f(ent); return; } if (level.intermissiontime) return; if ( Q_strcasecmp( cmd, "target" ) == 0 ) Cmd_Target_f( ent ); else if ( Q_strcasecmp( cmd, "use" ) == 0 || Q_strcasecmp( cmd, "use_only" ) == 0 || Q_strcasecmp( cmd, "use_index" ) == 0 || Q_strcasecmp( cmd, "use_index_only" ) == 0 ) Cmd_Use_f( ent ); else if ( Q_strcasecmp( cmd, "drop" ) == 0 || Q_strcasecmp( cmd, "drop_index" ) == 0 ) Cmd_Drop_f( ent ); else if ( Q_strcasecmp( cmd, "give" ) == 0 ) Cmd_Give_f( ent ); else if ( Q_strcasecmp( cmd, "god" ) == 0 ) Cmd_God_f( ent ); else if (Q_strcasecmp(cmd, "immortal") == 0) Cmd_Immortal_f(ent); else if ( Q_strcasecmp( cmd, "setpoi" ) == 0 ) Cmd_SetPOI_f( ent ); else if ( Q_strcasecmp( cmd, "checkpoi" ) == 0 ) Cmd_CheckPOI_f( ent ); // Paril: cheats to help with dev else if ( Q_strcasecmp( cmd, "spawn" ) == 0 ) Cmd_Spawn_f( ent ); else if ( Q_strcasecmp( cmd, "teleport" ) == 0 ) Cmd_Teleport_f( ent ); else if ( Q_strcasecmp( cmd, "notarget" ) == 0 ) Cmd_Notarget_f( ent ); else if ( Q_strcasecmp( cmd, "novisible" ) == 0 ) Cmd_Novisible_f( ent ); else if ( Q_strcasecmp( cmd, "alertall" ) == 0 ) Cmd_AlertAll_f( ent ); else if ( Q_strcasecmp( cmd, "noclip" ) == 0 ) Cmd_Noclip_f( ent ); else if ( Q_strcasecmp( cmd, "inven" ) == 0 ) Cmd_Inven_f( ent ); else if ( Q_strcasecmp( cmd, "invnext" ) == 0 ) SelectNextItem( ent, IF_ANY ); else if ( Q_strcasecmp( cmd, "invprev" ) == 0 ) SelectPrevItem( ent, IF_ANY ); else if ( Q_strcasecmp( cmd, "invnextw" ) == 0 ) SelectNextItem( ent, IF_WEAPON ); else if ( Q_strcasecmp( cmd, "invprevw" ) == 0 ) SelectPrevItem( ent, IF_WEAPON ); else if ( Q_strcasecmp( cmd, "invnextp" ) == 0 ) SelectNextItem( ent, IF_POWERUP ); else if ( Q_strcasecmp( cmd, "invprevp" ) == 0 ) SelectPrevItem( ent, IF_POWERUP ); else if ( Q_strcasecmp( cmd, "invuse" ) == 0 ) Cmd_InvUse_f( ent ); else if ( Q_strcasecmp( cmd, "invdrop" ) == 0 ) Cmd_InvDrop_f( ent ); else if ( Q_strcasecmp( cmd, "weapprev" ) == 0 ) Cmd_WeapPrev_f( ent ); else if ( Q_strcasecmp( cmd, "weapnext" ) == 0 ) Cmd_WeapNext_f( ent ); else if ( Q_strcasecmp( cmd, "weaplast" ) == 0 || Q_strcasecmp( cmd, "lastweap" ) == 0 ) Cmd_WeapLast_f( ent ); else if ( Q_strcasecmp( cmd, "kill" ) == 0 ) Cmd_Kill_f( ent ); else if ( Q_strcasecmp( cmd, "kill_ai" ) == 0 ) Cmd_Kill_AI_f( ent ); else if ( Q_strcasecmp( cmd, "where" ) == 0 ) Cmd_Where_f( ent ); else if ( Q_strcasecmp( cmd, "clear_ai_enemy" ) == 0 ) Cmd_Clear_AI_Enemy_f( ent ); else if (Q_strcasecmp(cmd, "putaway") == 0) Cmd_PutAway_f(ent); else if (Q_strcasecmp(cmd, "wave") == 0) Cmd_Wave_f(ent); else if (Q_strcasecmp(cmd, "playerlist") == 0) Cmd_PlayerList_f(ent); // ZOID else if (Q_strcasecmp(cmd, "team") == 0) CTFTeam_f(ent); else if (Q_strcasecmp(cmd, "id") == 0) CTFID_f(ent); else if (Q_strcasecmp(cmd, "yes") == 0) CTFVoteYes(ent); else if (Q_strcasecmp(cmd, "no") == 0) CTFVoteNo(ent); else if (Q_strcasecmp(cmd, "ready") == 0) CTFReady(ent); else if (Q_strcasecmp(cmd, "notready") == 0) CTFNotReady(ent); else if (Q_strcasecmp(cmd, "ghost") == 0) CTFGhost(ent); else if (Q_strcasecmp(cmd, "admin") == 0) CTFAdmin(ent); else if (Q_strcasecmp(cmd, "stats") == 0) CTFStats(ent); else if (Q_strcasecmp(cmd, "warp") == 0) CTFWarp(ent); else if (Q_strcasecmp(cmd, "boot") == 0) CTFBoot(ent); else if (Q_strcasecmp(cmd, "playerlist") == 0) CTFPlayerList(ent); else if (Q_strcasecmp(cmd, "observer") == 0) CTFObserver(ent); // ZOID else if (Q_strcasecmp(cmd, "switchteam") == 0) Cmd_Switchteam_f(ent); #ifndef KEX_Q2_GAME else // anything that doesn't match a command will be a chat Cmd_Say_f(ent, true); #else // anything that doesn't match a command will inform them else gi.LocClient_Print(ent, PRINT_HIGH, "invalid game command \"{}\"\n", gi.argv(0)); #endif }