quake2-rerelease-dll/rerelease/g_main.cpp
2023-10-03 14:43:06 -04:00

1054 lines
27 KiB
C++

// Copyright (c) ZeniMax Media Inc.
// Licensed under the GNU General Public License 2.0.
#include "g_local.h"
#include "bots/bot_includes.h"
CHECK_GCLIENT_INTEGRITY;
CHECK_EDICT_INTEGRITY;
std::mt19937 mt_rand;
game_locals_t game;
level_locals_t level;
local_game_import_t gi;
/*static*/ char local_game_import_t::print_buffer[0x10000];
/*static*/ std::array<char[MAX_INFO_STRING], MAX_LOCALIZATION_ARGS> local_game_import_t::buffers;
/*static*/ std::array<const char*, MAX_LOCALIZATION_ARGS> local_game_import_t::buffer_ptrs;
game_export_t globals;
spawn_temp_t st;
cached_modelindex sm_meat_index;
cached_soundindex snd_fry;
edict_t *g_edicts;
cvar_t *deathmatch;
cvar_t *coop;
cvar_t *skill;
cvar_t *fraglimit;
cvar_t *timelimit;
// ZOID
cvar_t *capturelimit;
cvar_t *g_quick_weapon_switch;
cvar_t *g_instant_weapon_switch;
// ZOID
cvar_t *password;
cvar_t *spectator_password;
cvar_t *needpass;
static cvar_t *maxclients;
cvar_t *maxspectators;
static cvar_t *maxentities;
cvar_t *g_select_empty;
cvar_t *sv_dedicated;
cvar_t *filterban;
cvar_t *sv_maxvelocity;
cvar_t *sv_gravity;
cvar_t *g_skipViewModifiers;
cvar_t *sv_rollspeed;
cvar_t *sv_rollangle;
cvar_t *gun_x;
cvar_t *gun_y;
cvar_t *gun_z;
cvar_t *run_pitch;
cvar_t *run_roll;
cvar_t *bob_up;
cvar_t *bob_pitch;
cvar_t *bob_roll;
cvar_t *sv_cheats;
cvar_t *g_debug_monster_paths;
cvar_t *g_debug_monster_kills;
cvar_t *bot_debug_follow_actor;
cvar_t *bot_debug_move_to_point;
cvar_t *flood_msgs;
cvar_t *flood_persecond;
cvar_t *flood_waitdelay;
cvar_t *sv_stopspeed; // PGM (this was a define in g_phys.c)
cvar_t *g_strict_saves;
// ROGUE cvars
cvar_t *gamerules;
cvar_t *huntercam;
cvar_t *g_dm_strong_mines;
cvar_t *g_dm_random_items;
// ROGUE
// [Kex]
cvar_t* g_instagib;
cvar_t* g_coop_player_collision;
cvar_t* g_coop_squad_respawn;
cvar_t* g_coop_enable_lives;
cvar_t* g_coop_num_lives;
cvar_t* g_coop_instanced_items;
cvar_t* g_allow_grapple;
cvar_t* g_grapple_fly_speed;
cvar_t* g_grapple_pull_speed;
cvar_t* g_grapple_damage;
cvar_t* g_coop_health_scaling;
cvar_t* g_weapon_respawn_time;
// dm"flags"
cvar_t* g_no_health;
cvar_t* g_no_items;
cvar_t* g_dm_weapons_stay;
cvar_t* g_dm_no_fall_damage;
cvar_t* g_dm_instant_items;
cvar_t* g_dm_same_level;
cvar_t* g_friendly_fire;
cvar_t* g_dm_force_respawn;
cvar_t* g_dm_force_respawn_time;
cvar_t* g_dm_spawn_farthest;
cvar_t* g_no_armor;
cvar_t* g_dm_allow_exit;
cvar_t* g_infinite_ammo;
cvar_t* g_dm_no_quad_drop;
cvar_t* g_dm_no_quadfire_drop;
cvar_t* g_no_mines;
cvar_t* g_dm_no_stack_double;
cvar_t* g_no_nukes;
cvar_t* g_no_spheres;
cvar_t* g_teamplay_armor_protect;
cvar_t* g_allow_techs;
cvar_t* g_start_items;
cvar_t* g_map_list;
cvar_t* g_map_list_shuffle;
cvar_t *g_lag_compensation;
cvar_t *sv_airaccelerate;
cvar_t *g_damage_scale;
cvar_t *g_disable_player_collision;
cvar_t *ai_damage_scale;
cvar_t *ai_model_scale;
cvar_t *ai_allow_dm_spawn;
cvar_t *ai_movement_disabled;
static cvar_t *g_frames_per_frame;
void SpawnEntities(const char *mapname, const char *entities, const char *spawnpoint);
void ClientThink(edict_t *ent, usercmd_t *cmd);
edict_t *ClientChooseSlot(const char *userinfo, const char *social_id, bool isBot, edict_t **ignore, size_t num_ignore, bool cinematic);
bool ClientConnect(edict_t *ent, char *userinfo, const char *social_id, bool isBot);
char *WriteGameJson(bool autosave, size_t *out_size);
void ReadGameJson(const char *jsonString);
char *WriteLevelJson(bool transition, size_t *out_size);
void ReadLevelJson(const char *jsonString);
bool G_CanSave();
void ClientDisconnect(edict_t *ent);
void ClientBegin(edict_t *ent);
void ClientCommand(edict_t *ent);
void G_RunFrame(bool main_loop);
void G_PrepFrame();
void InitSave();
#include <chrono>
/*
============
PreInitGame
This will be called when the dll is first loaded, which
only happens when a new game is started or a save game
is loaded.
============
*/
void PreInitGame()
{
maxclients = gi.cvar("maxclients", G_Fmt("{}", MAX_SPLIT_PLAYERS).data(), CVAR_SERVERINFO | CVAR_LATCH);
deathmatch = gi.cvar("deathmatch", "0", CVAR_LATCH);
coop = gi.cvar("coop", "0", CVAR_LATCH);
teamplay = gi.cvar("teamplay", "0", CVAR_LATCH);
// ZOID
CTFInit();
// ZOID
// ZOID
// This gamemode only supports deathmatch
if (ctf->integer)
{
if (!deathmatch->integer)
{
gi.Com_Print("Forcing deathmatch.\n");
gi.cvar_set("deathmatch", "1");
}
// force coop off
if (coop->integer)
gi.cvar_set("coop", "0");
// force tdm off
if (teamplay->integer)
gi.cvar_set("teamplay", "0");
}
if (teamplay->integer)
{
if (!deathmatch->integer)
{
gi.Com_Print("Forcing deathmatch.\n");
gi.cvar_set("deathmatch", "1");
}
// force coop off
if (coop->integer)
gi.cvar_set("coop", "0");
}
// ZOID
}
/*
============
InitGame
Called after PreInitGame when the game has set up cvars.
============
*/
void InitGame()
{
gi.Com_Print("==== InitGame ====\n");
InitSave();
// seed RNG
mt_rand.seed((uint32_t) std::chrono::system_clock::now().time_since_epoch().count());
gun_x = gi.cvar("gun_x", "0", CVAR_NOFLAGS);
gun_y = gi.cvar("gun_y", "0", CVAR_NOFLAGS);
gun_z = gi.cvar("gun_z", "0", CVAR_NOFLAGS);
// FIXME: sv_ prefix is wrong for these
sv_rollspeed = gi.cvar("sv_rollspeed", "200", CVAR_NOFLAGS);
sv_rollangle = gi.cvar("sv_rollangle", "2", CVAR_NOFLAGS);
sv_maxvelocity = gi.cvar("sv_maxvelocity", "2000", CVAR_NOFLAGS);
sv_gravity = gi.cvar("sv_gravity", "800", CVAR_NOFLAGS);
g_skipViewModifiers = gi.cvar("g_skipViewModifiers", "0", CVAR_NOSET);
sv_stopspeed = gi.cvar("sv_stopspeed", "100", CVAR_NOFLAGS); // PGM - was #define in g_phys.c
// ROGUE
huntercam = gi.cvar("huntercam", "1", CVAR_SERVERINFO | CVAR_LATCH);
g_dm_strong_mines = gi.cvar("g_dm_strong_mines", "0", CVAR_NOFLAGS);
g_dm_random_items = gi.cvar("g_dm_random_items", "0", CVAR_NOFLAGS);
// ROGUE
// [Kex] Instagib
g_instagib = gi.cvar("g_instagib", "0", CVAR_NOFLAGS);
// [Paril-KEX]
g_coop_player_collision = gi.cvar("g_coop_player_collision", "0", CVAR_LATCH);
g_coop_squad_respawn = gi.cvar("g_coop_squad_respawn", "1", CVAR_LATCH);
g_coop_enable_lives = gi.cvar("g_coop_enable_lives", "0", CVAR_LATCH);
g_coop_num_lives = gi.cvar("g_coop_num_lives", "2", CVAR_LATCH);
g_coop_instanced_items = gi.cvar("g_coop_instanced_items", "1", CVAR_LATCH);
g_allow_grapple = gi.cvar("g_allow_grapple", "auto", CVAR_NOFLAGS);
g_grapple_fly_speed = gi.cvar("g_grapple_fly_speed", G_Fmt("{}", CTF_DEFAULT_GRAPPLE_SPEED).data(), CVAR_NOFLAGS);
g_grapple_pull_speed = gi.cvar("g_grapple_pull_speed", G_Fmt("{}", CTF_DEFAULT_GRAPPLE_PULL_SPEED).data(), CVAR_NOFLAGS);
g_grapple_damage = gi.cvar("g_grapple_damage", "10", CVAR_NOFLAGS);
g_debug_monster_paths = gi.cvar("g_debug_monster_paths", "0", CVAR_NOFLAGS);
g_debug_monster_kills = gi.cvar("g_debug_monster_kills", "0", CVAR_LATCH);
bot_debug_follow_actor = gi.cvar("bot_debug_follow_actor", "0", CVAR_NOFLAGS);
bot_debug_move_to_point = gi.cvar("bot_debug_move_to_point", "0", CVAR_NOFLAGS);
// noset vars
sv_dedicated = gi.cvar("dedicated", "0", CVAR_NOSET);
// latched vars
sv_cheats = gi.cvar("cheats",
#if defined(_DEBUG)
"1"
#else
"0"
#endif
, CVAR_SERVERINFO | CVAR_LATCH);
gi.cvar("gamename", GAMEVERSION, CVAR_SERVERINFO | CVAR_LATCH);
maxspectators = gi.cvar("maxspectators", "4", CVAR_SERVERINFO);
skill = gi.cvar("skill", "1", CVAR_LATCH);
maxentities = gi.cvar("maxentities", G_Fmt("{}", MAX_EDICTS).data(), CVAR_LATCH);
gamerules = gi.cvar("gamerules", "0", CVAR_LATCH); // PGM
// change anytime vars
fraglimit = gi.cvar("fraglimit", "0", CVAR_SERVERINFO);
timelimit = gi.cvar("timelimit", "0", CVAR_SERVERINFO);
// ZOID
capturelimit = gi.cvar("capturelimit", "0", CVAR_SERVERINFO);
g_quick_weapon_switch = gi.cvar("g_quick_weapon_switch", "1", CVAR_LATCH);
g_instant_weapon_switch = gi.cvar("g_instant_weapon_switch", "0", CVAR_LATCH);
// ZOID
password = gi.cvar("password", "", CVAR_USERINFO);
spectator_password = gi.cvar("spectator_password", "", CVAR_USERINFO);
needpass = gi.cvar("needpass", "0", CVAR_SERVERINFO);
filterban = gi.cvar("filterban", "1", CVAR_NOFLAGS);
g_select_empty = gi.cvar("g_select_empty", "0", CVAR_ARCHIVE);
run_pitch = gi.cvar("run_pitch", "0.002", CVAR_NOFLAGS);
run_roll = gi.cvar("run_roll", "0.005", CVAR_NOFLAGS);
bob_up = gi.cvar("bob_up", "0.005", CVAR_NOFLAGS);
bob_pitch = gi.cvar("bob_pitch", "0.002", CVAR_NOFLAGS);
bob_roll = gi.cvar("bob_roll", "0.002", CVAR_NOFLAGS);
// flood control
flood_msgs = gi.cvar("flood_msgs", "4", CVAR_NOFLAGS);
flood_persecond = gi.cvar("flood_persecond", "4", CVAR_NOFLAGS);
flood_waitdelay = gi.cvar("flood_waitdelay", "10", CVAR_NOFLAGS);
g_strict_saves = gi.cvar("g_strict_saves", "1", CVAR_NOFLAGS);
sv_airaccelerate = gi.cvar("sv_airaccelerate", "0", CVAR_NOFLAGS);
g_damage_scale = gi.cvar("g_damage_scale", "1", CVAR_NOFLAGS);
g_disable_player_collision = gi.cvar("g_disable_player_collision", "0", CVAR_NOFLAGS);
ai_damage_scale = gi.cvar("ai_damage_scale", "1", CVAR_NOFLAGS);
ai_model_scale = gi.cvar("ai_model_scale", "0", CVAR_NOFLAGS);
ai_allow_dm_spawn = gi.cvar("ai_allow_dm_spawn", "0", CVAR_NOFLAGS);
ai_movement_disabled = gi.cvar("ai_movement_disabled", "0", CVAR_NOFLAGS);
g_frames_per_frame = gi.cvar("g_frames_per_frame", "1", CVAR_NOFLAGS);
g_coop_health_scaling = gi.cvar("g_coop_health_scaling", "0", CVAR_LATCH);
g_weapon_respawn_time = gi.cvar("g_weapon_respawn_time", "30", CVAR_NOFLAGS);
// dm "flags"
g_no_health = gi.cvar("g_no_health", "0", CVAR_NOFLAGS);
g_no_items = gi.cvar("g_no_items", "0", CVAR_NOFLAGS);
g_dm_weapons_stay = gi.cvar("g_dm_weapons_stay", "0", CVAR_NOFLAGS);
g_dm_no_fall_damage = gi.cvar("g_dm_no_fall_damage", "0", CVAR_NOFLAGS);
g_dm_instant_items = gi.cvar("g_dm_instant_items", "1", CVAR_NOFLAGS);
g_dm_same_level = gi.cvar("g_dm_same_level", "0", CVAR_NOFLAGS);
g_friendly_fire = gi.cvar("g_friendly_fire", "0", CVAR_NOFLAGS);
g_dm_force_respawn = gi.cvar("g_dm_force_respawn", "0", CVAR_NOFLAGS);
g_dm_force_respawn_time = gi.cvar("g_dm_force_respawn_time", "0", CVAR_NOFLAGS);
g_dm_spawn_farthest = gi.cvar("g_dm_spawn_farthest", "1", CVAR_NOFLAGS);
g_no_armor = gi.cvar("g_no_armor", "0", CVAR_NOFLAGS);
g_dm_allow_exit = gi.cvar("g_dm_allow_exit", "0", CVAR_NOFLAGS);
g_infinite_ammo = gi.cvar("g_infinite_ammo", "0", CVAR_LATCH);
g_dm_no_quad_drop = gi.cvar("g_dm_no_quad_drop", "0", CVAR_NOFLAGS);
g_dm_no_quadfire_drop = gi.cvar("g_dm_no_quadfire_drop", "0", CVAR_NOFLAGS);
g_no_mines = gi.cvar("g_no_mines", "0", CVAR_NOFLAGS);
g_dm_no_stack_double = gi.cvar("g_dm_no_stack_double", "0", CVAR_NOFLAGS);
g_no_nukes = gi.cvar("g_no_nukes", "0", CVAR_NOFLAGS);
g_no_spheres = gi.cvar("g_no_spheres", "0", CVAR_NOFLAGS);
g_teamplay_force_join = gi.cvar("g_teamplay_force_join", "0", CVAR_NOFLAGS);
g_teamplay_armor_protect = gi.cvar("g_teamplay_armor_protect", "0", CVAR_NOFLAGS);
g_allow_techs = gi.cvar("g_allow_techs", "auto", CVAR_NOFLAGS);
g_start_items = gi.cvar("g_start_items", "", CVAR_LATCH);
g_map_list = gi.cvar("g_map_list", "", CVAR_NOFLAGS);
g_map_list_shuffle = gi.cvar("g_map_list_shuffle", "0", CVAR_NOFLAGS);
g_lag_compensation = gi.cvar("g_lag_compensation", "1", CVAR_NOFLAGS);
// items
InitItems();
game = {};
// initialize all entities for this game
game.maxentities = maxentities->integer;
g_edicts = (edict_t *) gi.TagMalloc(game.maxentities * sizeof(g_edicts[0]), TAG_GAME);
globals.edicts = g_edicts;
globals.max_edicts = game.maxentities;
// initialize all clients for this game
game.maxclients = maxclients->integer;
game.clients = (gclient_t *) gi.TagMalloc(game.maxclients * sizeof(game.clients[0]), TAG_GAME);
globals.num_edicts = game.maxclients + 1;
//======
// ROGUE
if (gamerules->integer)
InitGameRules(); // if there are game rules to set up, do so now.
// ROGUE
//======
// how far back we should support lag origins for
game.max_lag_origins = 20 * (0.1f / gi.frame_time_s);
game.lag_origins = (vec3_t *) gi.TagMalloc(game.maxclients * sizeof(vec3_t) * game.max_lag_origins, TAG_GAME);
}
//===================================================================
void ShutdownGame()
{
gi.Com_Print("==== ShutdownGame ====\n");
gi.FreeTags(TAG_LEVEL);
gi.FreeTags(TAG_GAME);
}
static void *G_GetExtension(const char *name)
{
return nullptr;
}
const shadow_light_data_t *GetShadowLightData(int32_t entity_number);
gtime_t FRAME_TIME_S;
gtime_t FRAME_TIME_MS;
/*
=================
GetGameAPI
Returns a pointer to the structure with all entry points
and global variables
=================
*/
Q2GAME_API game_export_t *GetGameAPI(game_import_t *import)
{
gi = *import;
FRAME_TIME_S = FRAME_TIME_MS = gtime_t::from_ms(gi.frame_time_ms);
globals.apiversion = GAME_API_VERSION;
globals.PreInit = PreInitGame;
globals.Init = InitGame;
globals.Shutdown = ShutdownGame;
globals.SpawnEntities = SpawnEntities;
globals.WriteGameJson = WriteGameJson;
globals.ReadGameJson = ReadGameJson;
globals.WriteLevelJson = WriteLevelJson;
globals.ReadLevelJson = ReadLevelJson;
globals.CanSave = G_CanSave;
globals.Pmove = Pmove;
globals.GetExtension = G_GetExtension;
globals.ClientChooseSlot = ClientChooseSlot;
globals.ClientThink = ClientThink;
globals.ClientConnect = ClientConnect;
globals.ClientUserinfoChanged = ClientUserinfoChanged;
globals.ClientDisconnect = ClientDisconnect;
globals.ClientBegin = ClientBegin;
globals.ClientCommand = ClientCommand;
globals.RunFrame = G_RunFrame;
globals.PrepFrame = G_PrepFrame;
globals.ServerCommand = ServerCommand;
globals.Bot_SetWeapon = Bot_SetWeapon;
globals.Bot_TriggerEdict = Bot_TriggerEdict;
globals.Bot_GetItemID = Bot_GetItemID;
globals.Bot_UseItem = Bot_UseItem;
globals.Edict_ForceLookAtPoint = Edict_ForceLookAtPoint;
globals.Bot_PickedUpItem = Bot_PickedUpItem;
globals.Entity_IsVisibleToPlayer = Entity_IsVisibleToPlayer;
globals.GetShadowLightData = GetShadowLightData;
globals.edict_size = sizeof(edict_t);
return &globals;
}
//======================================================================
/*
=================
ClientEndServerFrames
=================
*/
void ClientEndServerFrames()
{
edict_t *ent;
// calc the player views now that all pushing
// and damage has been added
for (uint32_t i = 0; i < game.maxclients; i++)
{
ent = g_edicts + 1 + i;
if (!ent->inuse || !ent->client)
continue;
ClientEndServerFrame(ent);
}
}
/*
=================
CreateTargetChangeLevel
Returns the created target changelevel
=================
*/
edict_t *CreateTargetChangeLevel(const char *map)
{
edict_t *ent;
ent = G_Spawn();
ent->classname = "target_changelevel";
Q_strlcpy(level.nextmap, map, sizeof(level.nextmap));
ent->map = level.nextmap;
return ent;
}
inline std::vector<std::string> str_split(const std::string_view &str, char by)
{
std::vector<std::string> out;
size_t start, end = 0;
while ((start = str.find_first_not_of(by, end)) != std::string_view::npos)
{
end = str.find(by, start);
out.push_back(std::string{str.substr(start, end - start)});
}
return out;
}
/*
=================
EndDMLevel
The timelimit or fraglimit has been exceeded
=================
*/
void EndDMLevel()
{
edict_t *ent;
// stay on same level flag
if (g_dm_same_level->integer)
{
BeginIntermission(CreateTargetChangeLevel(level.mapname));
return;
}
if (*level.forcemap)
{
BeginIntermission(CreateTargetChangeLevel(level.forcemap));
return;
}
// see if it's in the map list
if (*g_map_list->string)
{
const char *str = g_map_list->string;
char first_map[MAX_QPATH] { 0 };
char *map;
while (1)
{
map = COM_ParseEx(&str, " ");
if (!*map)
break;
if (Q_strcasecmp(map, level.mapname) == 0)
{
// it's in the list, go to the next one
map = COM_ParseEx(&str, " ");
if (!*map)
{
// end of list, go to first one
if (!first_map[0]) // there isn't a first one, same level
{
BeginIntermission(CreateTargetChangeLevel(level.mapname));
return;
}
else
{
// [Paril-KEX] re-shuffle if necessary
if (g_map_list_shuffle->integer)
{
auto values = str_split(g_map_list->string, ' ');
if (values.size() == 1)
{
// meh
BeginIntermission(CreateTargetChangeLevel(level.mapname));
return;
}
std::shuffle(values.begin(), values.end(), mt_rand);
// if the current map is the map at the front, push it to the end
if (values[0] == level.mapname)
std::swap(values[0], values[values.size() - 1]);
gi.cvar_forceset("g_map_list", fmt::format("{}", join_strings(values, " ")).data());
BeginIntermission(CreateTargetChangeLevel(values[0].c_str()));
return;
}
BeginIntermission(CreateTargetChangeLevel(first_map));
return;
}
}
else
{
BeginIntermission(CreateTargetChangeLevel(map));
return;
}
}
if (!first_map[0])
Q_strlcpy(first_map, map, sizeof(first_map));
}
}
if (level.nextmap[0]) // go to a specific map
{
BeginIntermission(CreateTargetChangeLevel(level.nextmap));
return;
}
// search for a changelevel
ent = G_FindByString<&edict_t::classname>(nullptr, "target_changelevel");
if (!ent)
{ // the map designer didn't include a changelevel,
// so create a fake ent that goes back to the same level
BeginIntermission(CreateTargetChangeLevel(level.mapname));
return;
}
BeginIntermission(ent);
}
/*
=================
CheckNeedPass
=================
*/
void CheckNeedPass()
{
int need;
static int32_t password_modified, spectator_password_modified;
// if password or spectator_password has changed, update needpass
// as needed
if (Cvar_WasModified(password, password_modified) || Cvar_WasModified(spectator_password, spectator_password_modified))
{
need = 0;
if (*password->string && Q_strcasecmp(password->string, "none"))
need |= 1;
if (*spectator_password->string && Q_strcasecmp(spectator_password->string, "none"))
need |= 2;
gi.cvar_set("needpass", G_Fmt("{}", need).data());
}
}
/*
=================
CheckDMRules
=================
*/
void CheckDMRules()
{
gclient_t *cl;
if (level.intermissiontime)
return;
if (!deathmatch->integer)
return;
// ZOID
if (ctf->integer && CTFCheckRules())
{
EndDMLevel();
return;
}
if (CTFInMatch())
return; // no checking in match mode
// ZOID
//=======
// ROGUE
if (gamerules->integer && DMGame.CheckDMRules)
{
if (DMGame.CheckDMRules())
return;
}
// ROGUE
//=======
if (timelimit->value)
{
if (level.time >= gtime_t::from_min(timelimit->value))
{
gi.LocBroadcast_Print(PRINT_HIGH, "$g_timelimit_hit");
EndDMLevel();
return;
}
}
if (fraglimit->integer)
{
// [Paril-KEX]
if (teamplay->integer)
{
CheckEndTDMLevel();
return;
}
for (uint32_t i = 0; i < game.maxclients; i++)
{
cl = game.clients + i;
if (!g_edicts[i + 1].inuse)
continue;
if (cl->resp.score >= fraglimit->integer)
{
gi.LocBroadcast_Print(PRINT_HIGH, "$g_fraglimit_hit");
EndDMLevel();
return;
}
}
}
}
/*
=============
ExitLevel
=============
*/
void ExitLevel()
{
// [Paril-KEX] N64 fade
if (level.intermission_fade)
{
level.intermission_fade_time = level.time + 1.3_sec;
level.intermission_fading = true;
return;
}
ClientEndServerFrames();
level.exitintermission = 0;
level.intermissiontime = 0_ms;
// [Paril-KEX] support for intermission completely wiping players
// back to default stuff
if (level.intermission_clear)
{
level.intermission_clear = false;
for (uint32_t i = 0; i < game.maxclients; i++)
{
// [Kex] Maintain user info to keep the player skin.
char userinfo[MAX_INFO_STRING];
memcpy(userinfo, game.clients[i].pers.userinfo, sizeof(userinfo));
game.clients[i].pers = game.clients[i].resp.coop_respawn = {};
g_edicts[i + 1].health = 0; // this should trip the power armor, etc to reset as well
memcpy(game.clients[i].pers.userinfo, userinfo, sizeof(userinfo));
memcpy(game.clients[i].resp.coop_respawn.userinfo, userinfo, sizeof(userinfo));
}
}
// [Paril-KEX] end of unit, so clear level trackers
if (level.intermission_eou)
{
game.level_entries = {};
// give all players their lives back
if (g_coop_enable_lives->integer)
for (auto player : active_players())
player->client->pers.lives = g_coop_num_lives->integer + 1;
}
if (CTFNextMap())
return;
if (level.changemap == nullptr)
{
gi.Com_Error("Got null changemap when trying to exit level. Was a trigger_changelevel configured correctly?");
return;
}
// for N64 mainly, but if we're directly changing to "victorXXX.pcx" then
// end game
size_t start_offset = (level.changemap[0] == '*' ? 1 : 0);
if (strlen(level.changemap) > (6 + start_offset) &&
!Q_strncasecmp(level.changemap + start_offset, "victor", 6) &&
!Q_strncasecmp(level.changemap + strlen(level.changemap) - 4, ".pcx", 4))
gi.AddCommandString(G_Fmt("endgame \"{}\"\n", level.changemap + start_offset).data());
else
gi.AddCommandString(G_Fmt("gamemap \"{}\"\n", level.changemap).data());
level.changemap = nullptr;
}
static void G_CheckCvars()
{
if (Cvar_WasModified(sv_airaccelerate, game.airacceleration_modified))
{
// [Paril-KEX] air accel handled by game DLL now, and allow
// it to be changed in sp/coop
gi.configstring(CS_AIRACCEL, G_Fmt("{}", sv_airaccelerate->integer).data());
pm_config.airaccel = sv_airaccelerate->integer;
}
if (Cvar_WasModified(sv_gravity, game.gravity_modified))
level.gravity = sv_gravity->value;
}
static bool G_AnyDeadPlayersWithoutLives()
{
for (auto player : active_players())
if (player->health <= 0 && !player->client->pers.lives)
return true;
return false;
}
/*
================
G_RunFrame
Advances the world by 0.1 seconds
================
*/
inline void G_RunFrame_(bool main_loop)
{
level.in_frame = true;
G_CheckCvars();
Bot_UpdateDebug();
level.time += FRAME_TIME_MS;
if (level.intermission_fading)
{
if (level.intermission_fade_time > level.time)
{
float alpha = clamp(1.0f - (level.intermission_fade_time - level.time - 300_ms).seconds(), 0.f, 1.f);
for (auto player : active_players())
player->client->ps.screen_blend = { 0, 0, 0, alpha };
}
else
{
level.intermission_fade = level.intermission_fading = false;
ExitLevel();
}
level.in_frame = false;
return;
}
edict_t *ent;
// exit intermissions
if (level.exitintermission)
{
ExitLevel();
level.in_frame = false;
return;
}
// reload the map start save if restart time is set (all players are dead)
if (level.coop_level_restart_time > 0_ms && level.time > level.coop_level_restart_time)
{
ClientEndServerFrames();
gi.AddCommandString("restart_level\n");
}
// clear client coop respawn states; this is done
// early since it may be set multiple times for different
// players
if (coop->integer && (g_coop_enable_lives->integer || g_coop_squad_respawn->integer))
{
for (auto player : active_players())
{
if (player->client->respawn_time >= level.time)
player->client->coop_respawn_state = COOP_RESPAWN_WAITING;
else if (g_coop_enable_lives->integer && player->health <= 0 && player->client->pers.lives == 0)
player->client->coop_respawn_state = COOP_RESPAWN_NO_LIVES;
else if (g_coop_enable_lives->integer && G_AnyDeadPlayersWithoutLives())
player->client->coop_respawn_state = COOP_RESPAWN_NO_LIVES;
else
player->client->coop_respawn_state = COOP_RESPAWN_NONE;
}
}
//
// treat each object in turn
// even the world gets a chance to think
//
ent = &g_edicts[0];
for (uint32_t i = 0; i < globals.num_edicts; i++, ent++)
{
if (!ent->inuse)
{
// defer removing client info so that disconnected, etc works
if (i > 0 && i <= game.maxclients)
{
if (ent->timestamp && level.time < ent->timestamp)
{
int32_t playernum = ent - g_edicts - 1;
gi.configstring(CS_PLAYERSKINS + playernum, "");
ent->timestamp = 0_sec;
}
}
continue;
}
level.current_entity = ent;
// Paril: RF_BEAM entities update their old_origin by hand.
if (!(ent->s.renderfx & RF_BEAM))
ent->s.old_origin = ent->s.origin;
// if the ground entity moved, make sure we are still on it
if ((ent->groundentity) && (ent->groundentity->linkcount != ent->groundentity_linkcount))
{
contents_t mask = G_GetClipMask(ent);
if (!(ent->flags & (FL_SWIM | FL_FLY)) && (ent->svflags & SVF_MONSTER))
{
ent->groundentity = nullptr;
M_CheckGround(ent, mask);
}
else
{
// if it's still 1 point below us, we're good
trace_t tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, ent->s.origin + ent->gravityVector, ent,
mask);
if (tr.startsolid || tr.allsolid || tr.ent != ent->groundentity)
ent->groundentity = nullptr;
else
ent->groundentity_linkcount = ent->groundentity->linkcount;
}
}
Entity_UpdateState( ent );
if (i > 0 && i <= game.maxclients)
{
ClientBeginServerFrame(ent);
continue;
}
G_RunEntity(ent);
}
// see if it is time to end a deathmatch
CheckDMRules();
// see if needpass needs updated
CheckNeedPass();
if (coop->integer && (g_coop_enable_lives->integer || g_coop_squad_respawn->integer))
{
// rarely, we can see a flash of text if all players respawned
// on some other player, so if everybody is now alive we'll reset
// back to empty
bool reset_coop_respawn = true;
for (auto player : active_players())
{
if (player->health >= 0)
{
reset_coop_respawn = false;
break;
}
}
if (reset_coop_respawn)
{
for (auto player : active_players())
player->client->coop_respawn_state = COOP_RESPAWN_NONE;
}
}
// build the playerstate_t structures for all players
ClientEndServerFrames();
// [Paril-KEX] if not in intermission and player 1 is loaded in
// the game as an entity, increase timer on current entry
if (level.entry && !level.intermissiontime && g_edicts[1].inuse && g_edicts[1].client->pers.connected)
level.entry->time += FRAME_TIME_S;
// [Paril-KEX] run monster pains now
for (uint32_t i = 0; i < globals.num_edicts + 1 + game.maxclients + BODY_QUEUE_SIZE; i++)
{
edict_t *e = &g_edicts[i];
if (!e->inuse || !(e->svflags & SVF_MONSTER))
continue;
M_ProcessPain(e);
}
level.in_frame = false;
}
inline bool G_AnyPlayerSpawned()
{
for (auto player : active_players())
if (player->client && player->client->pers.spawned)
return true;
return false;
}
void G_RunFrame(bool main_loop)
{
if (main_loop && !G_AnyPlayerSpawned())
return;
for (int32_t i = 0; i < g_frames_per_frame->integer; i++)
G_RunFrame_(main_loop);
// match details.. only bother if there's at least 1 player in-game
// and not already end of game
if (G_AnyPlayerSpawned() && !level.intermissiontime)
{
constexpr gtime_t MATCH_REPORT_TIME = 45_sec;
if (level.time - level.next_match_report > MATCH_REPORT_TIME)
{
level.next_match_report = level.time + MATCH_REPORT_TIME;
G_ReportMatchDetails(false);
}
}
}
/*
================
G_PrepFrame
This has to be done before the world logic, because
player processing happens outside RunFrame
================
*/
void G_PrepFrame()
{
for (uint32_t i = 0; i < globals.num_edicts; i++)
g_edicts[i].s.event = EV_NONE;
for (auto player : active_players())
player->client->ps.stats[STAT_HIT_MARKER] = 0;
globals.server_flags &= ~SERVER_FLAG_INTERMISSION;
if ( level.intermissiontime ) {
globals.server_flags |= SERVER_FLAG_INTERMISSION;
}
}