diff --git a/.gitignore b/.gitignore index 5d3374bbb..a49facb52 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ build.sh /build_win clang-build.sh -/bots \ No newline at end of file +/bots +/dev +.DS_Store \ No newline at end of file diff --git a/doc/action.md b/doc/action.md index c556c2e48..15aaa9ddb 100644 --- a/doc/action.md +++ b/doc/action.md @@ -571,7 +571,7 @@ For the lazy players under us, we have created three new commands to make things **Commands:** - `auto_join [0/1/2]` - when on (1), the players will automatically join the team he was in during the last map. If set to 2, it will automatically join the team with the least players, or randomly if equal. - `auto_equip [0/1]` - when on (1), the players will automatically be given the equipment he had during the last map. -- `auto_menu [0/1]` - when on (1), the menu will automatically open when you join the server. +- `auto_menu [0/1/2]` - when on (1), the menu will automatically open when you join the server. when set to (2), it will automatically render the menu AFTER the MOTD renders ### Automatic Demo Recording To ease the recording of demos by a client over several maps, we have implemented a feature which will let the client automatically record a demo on each new map. When enabled, the server will keep track of the recording of the client, and will create a demo of the map with the following name: date_time-map.dm2 In matchmode, the demo's name will be: date_time-team1_vs_team2-map.dm2 diff --git a/inc/common/cvar.h b/inc/common/cvar.h index 4c6253617..21b37300e 100644 --- a/inc/common/cvar.h +++ b/inc/common/cvar.h @@ -49,7 +49,7 @@ interface from being ambiguous. #define CVAR_INFOMASK (CVAR_USERINFO | CVAR_SERVERINFO) #define CVAR_MODIFYMASK (CVAR_INFOMASK | CVAR_FILES | CVAR_REFRESH | CVAR_SOUND) #define CVAR_NOARCHIVEMASK (CVAR_NOSET | CVAR_CHEAT | CVAR_PRIVATE | CVAR_ROM | CVAR_NOARCHIVE) -#define CVAR_EXTENDED_MASK (~MASK(5)) +#define CVAR_EXTENDED_MASK ((~MASK(5)) & ~CVAR_SERVERINFO_EXT) extern cvar_t *cvar_vars; extern int cvar_modified; diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 51a0b7bd5..44773af2a 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -907,12 +907,13 @@ CVARS (console variables) #ifndef CVAR #define CVAR -#define CVAR_ARCHIVE BIT(0) // set to cause it to be saved to vars.rc -#define CVAR_USERINFO BIT(1) // added to userinfo when changed -#define CVAR_SERVERINFO BIT(2) // added to serverinfo when changed -#define CVAR_NOSET BIT(3) // don't allow change from console at all, - // but can be set from the command line -#define CVAR_LATCH BIT(4) // save changes until server restart +#define CVAR_ARCHIVE BIT(0) // set to cause it to be saved to vars.rc +#define CVAR_USERINFO BIT(1) // added to userinfo when changed +#define CVAR_SERVERINFO BIT(2) // added to serverinfo when changed +#define CVAR_NOSET BIT(3) // don't allow change from console at all, + // but can be set from the command line +#define CVAR_LATCH BIT(4) // save changes until server restart +#define CVAR_SERVERINFO_EXT BIT(16) // queryable via rulesext protocol #if USE_CLIENT || USE_SERVER struct cvar_s; diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 324a14296..7cb149601 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -791,6 +791,10 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) else ctfgame.team2++; + // Update team scores + teams[TEAM1].score = ctfgame.team1; + teams[TEAM2].score = ctfgame.team2; + CTFDynamicRespawnTimer(); // Dynamic respawn time gi.sound(ent, CHAN_RELIABLE + CHAN_NO_PHS_ADD + CHAN_VOICE, diff --git a/src/action/g_save.c b/src/action/g_save.c index 4d5413173..f82d07dfb 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -348,15 +348,15 @@ void InitGame( void ) dedicated = gi.cvar( "dedicated", "0", CVAR_NOSET ); steamid = gi.cvar( "steamid", "0", CVAR_NOSET ); - sv_cheats = gi.cvar( "cheats", "0", CVAR_SERVERINFO | CVAR_LATCH ); - gi.cvar( "gamename", GAMEVERSION, CVAR_SERVERINFO | CVAR_NOSET ); // Removed it from Serverinfo, we already have game and gamedir - gi.cvar( "gamedate", __DATE__, CVAR_SERVERINFO | CVAR_NOSET ); - actionversion = gi.cvar( "actionversion", "TNG " VERSION, CVAR_SERVERINFO | CVAR_NOSET ); + sv_cheats = gi.cvar( "cheats", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH ); + gi.cvar( "gamename", GAMEVERSION, CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET ); // Removed it from Serverinfo, we already have game and gamedir + gi.cvar( "gamedate", __DATE__, CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET ); + actionversion = gi.cvar( "actionversion", "TNG " VERSION, CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET ); gi.cvar_set( "actionversion", "TNG " VERSION ); net_port = gi.cvar( "net_port", "27910", CVAR_NOSET ); - maxclients = gi.cvar( "maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH ); + maxclients = gi.cvar( "maxclients", "8", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH ); maxentities = gi.cvar( "maxentities", "1024", CVAR_LATCH ); deathmatch = gi.cvar( "deathmatch", "1", CVAR_LATCH ); @@ -364,25 +364,25 @@ void InitGame( void ) gi.dprintf( "Turning deathmatch on.\n" ); gi.cvar_forceset( "deathmatch", "1" ); } - coop = gi.cvar( "coop", "0", CVAR_LATCH ); + coop = gi.cvar( "coop", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); // if (cv->value) { // gi.dprintf( "Turning coop off.\n" ); // gi.cvar_forceset( "coop", "0" ); // } - dmflags = gi.cvar( "dmflags", "0", CVAR_SERVERINFO ); - fraglimit = gi.cvar( "fraglimit", "0", CVAR_SERVERINFO ); - timelimit = gi.cvar( "timelimit", "0", CVAR_SERVERINFO ); - maptime = gi.cvar("maptime", "0:00", CVAR_SERVERINFO | CVAR_NOSET); - capturelimit = gi.cvar( "capturelimit", "0", CVAR_SERVERINFO ); + dmflags = gi.cvar( "dmflags", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); + fraglimit = gi.cvar( "fraglimit", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); + timelimit = gi.cvar( "timelimit", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); + maptime = gi.cvar("maptime", "0:00", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET); + capturelimit = gi.cvar( "capturelimit", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); password = gi.cvar( "password", "", CVAR_USERINFO ); filterban = gi.cvar( "filterban", "1", 0 ); silenceban = gi.cvar( "silenceban", "1", 0); //rekkie -- silence ban - needpass = gi.cvar( "needpass", "0", CVAR_SERVERINFO ); + needpass = gi.cvar( "needpass", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); radiolog = gi.cvar( "radiolog", "0", 0 ); - teamplay = gi.cvar( "teamplay", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gm' (gamemode) + teamplay = gi.cvar( "teamplay", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT); //Removed in favor of 'gm' (gamemode) motd_time = gi.cvar( "motd_time", "2", 0 ); - hostname = gi.cvar( "hostname", "unnamed", CVAR_SERVERINFO ); + hostname = gi.cvar( "hostname", "unnamed", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); strtwpn = gi.cvar( "dmweapon", MK23_NAME, 0 ); actionmaps = gi.cvar( "actionmaps", "1", 0 ); if (actionmaps->value && num_maps < 1) @@ -393,10 +393,10 @@ void InitGame( void ) nohud = gi.cvar( "nohud", "0", CVAR_LATCH ); hud_team_icon = gi.cvar( "hud_team_icon", "0", 0 ); hud_items_cycle = gi.cvar( "hud_items_cycle", "20", 0 ); - roundlimit = gi.cvar( "roundlimit", "0", CVAR_SERVERINFO ); - limchasecam = gi.cvar( "limchasecam", "0", CVAR_LATCH ); + roundlimit = gi.cvar( "roundlimit", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); + limchasecam = gi.cvar( "limchasecam", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); skipmotd = gi.cvar( "skipmotd", "0", 0 ); - roundtimelimit = gi.cvar( "roundtimelimit", "0", CVAR_SERVERINFO ); + roundtimelimit = gi.cvar( "roundtimelimit", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); maxteamkills = gi.cvar( "maxteamkills", "0", 0 ); twbanrounds = gi.cvar( "twbanrounds", "2", 0 ); tkbanrounds = gi.cvar( "tkbanrounds", "2", 0 ); @@ -407,15 +407,15 @@ void InitGame( void ) use_voice = gi.cvar( "use_voice", "1", 0 ); //slicer ppl_idletime = gi.cvar( "ppl_idletime", "15", 0 ); use_buggy_bandolier = gi.cvar( "use_buggy_bandolier", "0", 0 ); - use_tourney = gi.cvar( "use_tourney", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gm' (gamemode) - use_3teams = gi.cvar( "use_3teams", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gmf' (gamemodeflags) - use_randoms = gi.cvar( "use_randoms", "0", CVAR_SERVERINFO | CVAR_LATCH ); // Random weapons and items mode + use_tourney = gi.cvar( "use_tourney", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT ); //Removed in favor of 'gm' (gamemode) + use_3teams = gi.cvar( "use_3teams", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT); //Removed in favor of 'gmf' (gamemodeflags) + use_randoms = gi.cvar( "use_randoms", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH | CVAR_SERVERINFO_EXT); // Random weapons and items mode use_kickvote = gi.cvar( "use_kickvote", "1", 0 ); //slicer use_mapvote = gi.cvar( "use_mapvote", "1", 0 ); //slicer use_scramblevote = gi.cvar( "use_scramblevote", "1", 0 ); //slicer - ctf = gi.cvar( "ctf", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gm' (gamemode) + ctf = gi.cvar( "ctf", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT); //Removed in favor of 'gm' (gamemode) ctf_forcejoin = gi.cvar( "ctf_forcejoin", "", 0 ); - ctf_mode = gi.cvar( "ctf_mode", "0", CVAR_LATCH ); // Repurposed in 2025 -- now toggles between CTF and CTB modes. Rewards now handled via `ctf_rewards` cvar + ctf_mode = gi.cvar( "ctf_mode", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); // Repurposed in 2025 -- now toggles between CTF and CTB modes. Rewards now handled via `ctf_rewards` cvar ctf_dropflag = gi.cvar( "ctf_dropflag", "1", 0 ); ctf_respawn = gi.cvar( "ctf_respawn", "4", 0 ); ctf_model = gi.cvar( "ctf_model", "male", CVAR_LATCH ); @@ -425,8 +425,8 @@ void InitGame( void ) medkit_instant = gi.cvar( "medkit_instant", "0", 0 ); medkit_max = gi.cvar( "medkit_max", "3", 0 ); medkit_value = gi.cvar( "medkit_value", "25", 0 ); - dom = gi.cvar( "dom", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gmf' (gamemodeflags) - use_grapple = gi.cvar( "use_grapple", "0", 0 ); + dom = gi.cvar( "dom", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT); //Removed in favor of 'gmf' (gamemodeflags) + use_grapple = gi.cvar( "use_grapple", "0", CVAR_SERVERINFO_EXT ); mv_public = gi.cvar( "mv_public", "0", 0 ); //slicer vk_public = gi.cvar( "vk_public", "0", 0 ); //slicer punishkills = gi.cvar( "punishkills", "1", 0 ); //slicer @@ -439,7 +439,7 @@ void InitGame( void ) rrot = gi.cvar( "rrot", "0", 0 ); empty_rotate = gi.cvar( "empty_rotate", "0", 0 ); empty_exec = gi.cvar( "empty_exec", "", 0 ); - llsound = gi.cvar( "llsound", "1", 0 ); + llsound = gi.cvar( "llsound", "1", CVAR_SERVERINFO_EXT ); loud_guns = gi.cvar( "loud_guns", "0", 0 ); sync_guns = gi.cvar( "sync_guns", "1", 0 ); silentwalk = gi.cvar( "silentwalk", "0", 0 ); @@ -457,18 +457,18 @@ void InitGame( void ) video_check_lockpvs = gi.cvar( "video_check_lockpvs", "0", 0 ); video_check_glclear = gi.cvar( "video_check_glclear", "0", 0 ); video_checktime = gi.cvar( "video_checktime", "15", 0 ); - hc_single = gi.cvar( "hc_single", "1", CVAR_LATCH ); //default ON - hc_boost = gi.cvar("hc_boost", "0", CVAR_LATCH); //rekkie -- allow HC to 'boost' the player + hc_single = gi.cvar( "hc_single", "1", CVAR_LATCH | CVAR_SERVERINFO_EXT ); //default ON + hc_boost = gi.cvar("hc_boost", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); //rekkie -- allow HC to 'boost' the player hc_boost_percent = gi.cvar("hc_boost_percent", "100", 0); //rekkie -- allow HC to 'boost' the player hc_silencer = gi.cvar("hc_silencer", "0", 0); //rekkie -- allow HC to 'boost' the player wp_flags = gi.cvar( "wp_flags", WPF_DEFAULT_STR, 0 ); itm_flags = gi.cvar( "itm_flags", ITF_DEFAULT_STR, 0 ); - matchmode = gi.cvar( "matchmode", "0", CVAR_SERVERINFO | CVAR_LATCH ); + matchmode = gi.cvar( "matchmode", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH ); hearall = gi.cvar( "hearall", "0", 0 ); // used in matchmode deadtalk = gi.cvar( "deadtalk", "0", 0 ); force_skin = gi.cvar( "force_skin", "", 0 ); - teamdm = gi.cvar( "teamdm", "0", CVAR_LATCH ); + teamdm = gi.cvar( "teamdm", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT ); teamdm_respawn = gi.cvar( "teamdm_respawn", "2", 0 ); respawn_effect = gi.cvar( "respawn_effect", "0", 0 ); @@ -486,26 +486,26 @@ void InitGame( void ) mm_pausecount = gi.cvar( "mm_allowcount", "3", CVAR_LATCH ); mm_pausetime = gi.cvar( "mm_pausetime", "2", CVAR_LATCH ); - teams[TEAM1].teamscore = gi.cvar("t1", "0", CVAR_SERVERINFO | CVAR_NOSET); - teams[TEAM2].teamscore = gi.cvar("t2", "0", CVAR_SERVERINFO | CVAR_NOSET); - teams[TEAM3].teamscore = gi.cvar("t3", "0", CVAR_SERVERINFO | CVAR_NOSET); + teams[TEAM1].teamscore = gi.cvar("t1", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET); + teams[TEAM2].teamscore = gi.cvar("t2", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET); + teams[TEAM3].teamscore = gi.cvar("t3", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET); stats_endmap = gi.cvar( "stats_endmap", "1", 0 ); stats_afterround = gi.cvar( "stats_afterround", "0", 0 ); auto_join = gi.cvar( "auto_join", "0", 0 ); auto_equip = gi.cvar( "auto_equip", "0", 0 ); auto_menu = gi.cvar( "auto_menu", "0", 0 ); - eventeams = gi.cvar( "eventeams", "0", 0 ); - use_balancer = gi.cvar( "use_balancer", "0", 0 ); - dm_choose = gi.cvar( "dm_choose", "0", CVAR_LATCH ); - dm_shield = gi.cvar( "dm_shield", "0", 0 ); + eventeams = gi.cvar( "eventeams", "0", CVAR_SERVERINFO_EXT ); + use_balancer = gi.cvar( "use_balancer", "0", CVAR_SERVERINFO_EXT ); + dm_choose = gi.cvar( "dm_choose", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT ); + dm_shield = gi.cvar( "dm_shield", "0", CVAR_SERVERINFO_EXT ); tourney_lca = gi.cvar( "tourney_lca", "0", 0 ); - use_punch = gi.cvar( "use_punch", "1", 0 ); + use_punch = gi.cvar( "use_punch", "1", CVAR_SERVERINFO_EXT); //TNG:Freud - new spawning system use_oldspawns = gi.cvar( "use_oldspawns", "0", CVAR_LATCH ); //TNG:Freud - ghosts - use_ghosts = gi.cvar( "use_ghosts", "0", CVAR_LATCH ); + use_ghosts = gi.cvar( "use_ghosts", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); radio_max = gi.cvar( "radio_max", "3", 0 ); radio_time = gi.cvar( "radio_time", "2", 0 ); @@ -513,14 +513,14 @@ void InitGame( void ) //SLIC2 radio_repeat_time = gi.cvar( "radio_repeat_time", "1", 0 ); radio_repeat = gi.cvar( "radio_repeat", "2", 0 ); - unique_weapons = gi.cvar( "weapons", "1", CVAR_SERVERINFO | CVAR_LATCH ); // zucc changed teamplay to 1 - unique_items = gi.cvar( "items", "1", CVAR_SERVERINFO | CVAR_LATCH ); + unique_weapons = gi.cvar( "weapons", "1", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH ); // zucc changed teamplay to 1 + unique_items = gi.cvar( "items", "1", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH ); ir = gi.cvar( "ir", "1", 0 ); knifelimit = gi.cvar( "knifelimit", "40", 0 ); - allweapon = gi.cvar( "allweapon", "0", 0 ); // Removed it from Serverinfo - allitem = gi.cvar( "allitem", "0", 0 ); // Removed it from Serverinfo + allweapon = gi.cvar( "allweapon", "0", CVAR_SERVERINFO_EXT ); // Removed it from Serverinfo + allitem = gi.cvar( "allitem", "0", CVAR_SERVERINFO_EXT); // Removed it from Serverinfo allow_hoarding = gi.cvar( "allow_hoarding", "0", CVAR_LATCH ); - tgren = gi.cvar( "tgren", "0", CVAR_SERVERINFO ); + tgren = gi.cvar( "tgren", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); //SLIC2 /*flashgren = gi.cvar ("flashgren", "1", 0); flashradius = gi.cvar ("flashradius", "300", 0); @@ -537,17 +537,17 @@ void InitGame( void ) day_cycle = gi.cvar( "day_cycle", "10", 0 ); // Darkmatch cycle time. use_flashlight = gi.cvar( "use_flashlight", "0", 0 ); use_classic = gi.cvar( "use_classic", "0", 0 ); // Reset Grenade Strength to 1.52 - use_gren_bonk = gi.cvar( "use_gren_bonk", "0", 0 ); // Grenade Bonk + use_gren_bonk = gi.cvar( "use_gren_bonk", "0", CVAR_SERVERINFO_EXT ); // Grenade Bonk //CGF_SFX_InstallGlassSupport(); // william for CGF (glass fx) - breakableglass = gi.cvar("breakableglass", "0", 0); + breakableglass = gi.cvar("breakableglass", "0", CVAR_SERVERINFO_EXT); glassfragmentlimit = gi.cvar("glassfragmentlimit", "30", 0); //CGF_SFX_InstallGlassSupport(); // william for CGF (glass fx) - grenade_drop = gi.cvar( "grenade_drop", "0", 0 ); // Raptor007 - added grenade drop + grenade_drop = gi.cvar( "grenade_drop", "0", CVAR_SERVERINFO_EXT ); // Raptor007 - added grenade drop g_select_empty = gi.cvar( "g_select_empty", "0", CVAR_ARCHIVE ); - g_protocol_extensions = gi.cvar("g_protocol_extensions", "0", CVAR_LATCH); + g_protocol_extensions = gi.cvar("g_protocol_extensions", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); run_pitch = gi.cvar( "run_pitch", "0.002", 0 ); run_roll = gi.cvar( "run_roll", "0.005", 0 ); @@ -583,17 +583,17 @@ void InitGame( void ) game.csr = cs_remap_old; } } - jump = gi.cvar ("jump", "0", /*CVAR_SERVERINFO|*/ CVAR_LATCH); // jumping mod -- removed from serverinfo 2022 + jump = gi.cvar ("jump", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT|*/ CVAR_LATCH | CVAR_SERVERINFO_EXT); // jumping mod -- removed from serverinfo 2022 - warmup = gi.cvar( "warmup", "0", CVAR_LATCH ); + warmup = gi.cvar( "warmup", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT ); warmup_bots = gi.cvar( "warmup_bots", "0", CVAR_LATCH ); - round_begin = gi.cvar( "round_begin", "15", 0 ); - spectator_hud = gi.cvar( "spectator_hud", "1", CVAR_LATCH ); + round_begin = gi.cvar( "round_begin", "15", CVAR_SERVERINFO_EXT ); + spectator_hud = gi.cvar( "spectator_hud", "1", CVAR_LATCH | CVAR_SERVERINFO_EXT); - use_mvd2 = gi.cvar( "use_mvd2", "0", 0 ); // JBravo: q2pro MVD2 recording. 0 = off, 1 = on + use_mvd2 = gi.cvar( "use_mvd2", "0", CVAR_SERVERINFO_EXT ); // JBravo: q2pro MVD2 recording. 0 = off, 1 = on // BEGIN AQ2 ETE - esp = gi.cvar( "esp", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gm' (gamemode) + esp = gi.cvar( "esp", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT); //Removed in favor of 'gm' (gamemode) esp_atl = gi.cvar( "esp_atl", "0", 0 ); // This forces ATL mode even if ETV mode is set in the .esp file esp_punish = gi.cvar("esp_punish", "0", 0); esp_etv_halftime = gi.cvar("esp_etv_halftime", "0", CVAR_LATCH); @@ -617,32 +617,32 @@ void InitGame( void ) // 2022 server_id = gi.cvar( "server_id", "", 0 ); // Removed it from Serverinfo - stat_logs = gi.cvar( "stat_logs", "0", CVAR_SERVERINFO); + stat_logs = gi.cvar( "stat_logs", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); if (stat_logs->value) { gi.dprintf( "stat_logs is enabled, forcing logfile_flush to 2\n" ); gi.cvar_forceset("logfile_flush", "2"); } - sv_antilag = gi.cvar("sv_antilag", "1", CVAR_SERVERINFO); - sv_antilag_interp = gi.cvar("sv_antilag_interp", "0", CVAR_SERVERINFO); - sv_limp_highping = gi.cvar("sv_limp_highping", "70", 0); // Removed it from Serverinfo + sv_antilag = gi.cvar("sv_antilag", "1", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); + sv_antilag_interp = gi.cvar("sv_antilag_interp", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); + sv_limp_highping = gi.cvar("sv_limp_highping", "70", CVAR_SERVERINFO_EXT); // Removed it from Serverinfo mapvote_next_limit = gi.cvar( "mapvote_next_limit", "0", 0); stat_apikey = gi.cvar("stat_apikey", "none", 0); // Never include this in serverinfo! stat_url = gi.cvar("stat_url", "https://apigateway.aq2world.com/api/v1/stats", 0); - gm = gi.cvar("gm", "dm", CVAR_SERVERINFO); - gmf = gi.cvar("gmf", "0", CVAR_SERVERINFO); - sv_idleremove = gi.cvar("sv_idleremove", "0", 0); + gm = gi.cvar("gm", "dm", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); + gmf = gi.cvar("gmf", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); + sv_idleremove = gi.cvar("sv_idleremove", "0", CVAR_SERVERINFO_EXT); g_spawn_items = gi.cvar("g_spawn_items", "0", CVAR_LATCH); // 2023 - use_killcounts = gi.cvar("use_killcounts", "0", 0); - zoom_comp = gi.cvar("zoom_comp", "1", 0); - item_kit_mode = gi.cvar("item_kit_mode", "0", CVAR_LATCH); - gun_dualmk23_enhance = gi.cvar("gun_dualmk23_enhance", "0", 0); + use_killcounts = gi.cvar("use_killcounts", "0", CVAR_SERVERINFO_EXT); + zoom_comp = gi.cvar("zoom_comp", "1", CVAR_SERVERINFO_EXT); + item_kit_mode = gi.cvar("item_kit_mode", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); + gun_dualmk23_enhance = gi.cvar("gun_dualmk23_enhance", "0", CVAR_SERVERINFO_EXT); printrules = gi.cvar("printrules", "0", 0); timedmsgs = gi.cvar("timedmsgs", "0", 0); - mm_captain_teamname = gi.cvar("mm_captain_teamname", "0", 0); - sv_killgib = gi.cvar("sv_killgib", "0", 0); + mm_captain_teamname = gi.cvar("mm_captain_teamname", "0", CVAR_SERVERINFO_EXT); + sv_killgib = gi.cvar("sv_killgib", "0", CVAR_SERVERINFO_EXT); // 2024 warmup_unready = gi.cvar("warmup_unready", "0", 0); @@ -662,7 +662,7 @@ void InitGame( void ) msgflags = gi.cvar("msgflags", "0", 0); use_pickup = gi.cvar("use_pickup", "0", 0); - training_mode = gi.cvar("training_mode", "0", CVAR_LATCH); + training_mode = gi.cvar("training_mode", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); if (training_mode->value){ gi.cvar_forceset("item_respawnmode", "1"); gi.cvar_forceset("items", "2"); @@ -671,18 +671,18 @@ void InitGame( void ) } g_highscores_dir = gi.cvar("g_highscores_dir", "highscores", 0); g_highscores_countbots = gi.cvar("g_highscores_countbots", "0", 0); - lca_grenade = gi.cvar("lca_grenade", "0", 0); - knife_catch = gi.cvar("knife_catch", "0", 0); + lca_grenade = gi.cvar("lca_grenade", "0", CVAR_SERVERINFO_EXT); + knife_catch = gi.cvar("knife_catch", "0", CVAR_SERVERINFO_EXT); // 2025 - ctf_rewards = gi.cvar("ctf_rewards", "1", 0); - bots = gi.cvar("bots", "0", CVAR_SERVERINFO); + ctf_rewards = gi.cvar("ctf_rewards", "1", CVAR_SERVERINFO_EXT); + bots = gi.cvar("bots", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); // new AQtion Extension cvars #ifdef AQTION_EXTENSION - use_newirvision = gi.cvar("use_newirvision", "1", 0); - use_indicators = gi.cvar("use_indicators", "1", 0); - use_xerp = gi.cvar("use_xerp", "1", 0); + use_newirvision = gi.cvar("use_newirvision", "1", CVAR_SERVERINFO_EXT); + use_indicators = gi.cvar("use_indicators", "1", CVAR_SERVERINFO_EXT); + use_xerp = gi.cvar("use_xerp", "1", CVAR_SERVERINFO_EXT); #endif // Discord SDK integration with Q2Pro @@ -702,14 +702,14 @@ void InitGame( void ) ltk_botfile = gi.cvar( "ltk_botfile", "botdata", 0); ltk_loadbots = gi.cvar( "ltk_loadbots", "1", 0); //rekkie -- DEV_1 -- s - bot_enable = gi.cvar("bot_enable", "0", CVAR_LATCH); + bot_enable = gi.cvar("bot_enable", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); bot_skill = gi.cvar("bot_skill", "7", 0); // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! bot_skill_threshold = gi.cvar("bot_skill_threshold", "0", 0); // Dynamic skill adjustment kicks in if a threshold has been hit bot_remember = gi.cvar("bot_remember", "15", 0); // How long (in seconds) the bot remembers an enemy after visibility has been lost bot_reaction = gi.cvar("bot_reaction", "0.5", 0); // How long (in seconds) until the bot reacts to an enemy in sight bot_showpath = gi.cvar("bot_showpath", "0", 0); bot_maxteam = gi.cvar("bot_maxteam", "0", 0); - bot_playercount = gi.cvar("bot_playercount", "0", 0); + bot_playercount = gi.cvar("bot_playercount", "0", CVAR_SERVERINFO_EXT); bot_rush = gi.cvar("bot_rush", "0", 0); bot_randvoice = gi.cvar("bot_randvoice", "5", 0); bot_randskill = gi.cvar("bot_randskill", "10", 0); @@ -717,13 +717,13 @@ void InitGame( void ) bot_chat = gi.cvar("bot_chat", "0", 0); bot_personality = gi.cvar("bot_personality", "0", CVAR_LATCH); bot_ragequit = gi.cvar("bot_ragequit", "0", 0); - bot_countashuman = gi.cvar("bot_countashuman", "0", 0); + bot_countashuman = gi.cvar("bot_countashuman", "0", CVAR_SERVERINFO_EXT); bot_debug = gi.cvar("bot_debug", "0", 0); bot_count_min = gi.cvar("bot_count_min", "0", 0); bot_count_max = gi.cvar("bot_count_max", "0", 0); bot_rotate = gi.cvar("bot_rotate", "0", 0); - bot_reportasclient = gi.cvar("bot_reportasclient", "0", CVAR_LATCH); - bot_reportpings = gi.cvar("bot_reportpings", "0", 0); + bot_reportasclient = gi.cvar("bot_reportasclient", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); + bot_reportpings = gi.cvar("bot_reportpings", "0", CVAR_SERVERINFO_EXT); bot_navautogen = gi.cvar("bot_navautogen", "0", 0); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); gl_shaders = gi.cvar("gl_shaders", "0", 0); @@ -773,7 +773,7 @@ void InitGame( void ) { gi.dprintf ("...server supports GMF_VARIABLE_FPS\n"); - cv = gi.cvar( "sv_fps", NULL, CVAR_SERVERINFO ); + cv = gi.cvar( "sv_fps", NULL, CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); if( cv ) { int framediv = (int) cv->value / BASE_FRAMERATE; diff --git a/src/server/main.c b/src/server/main.c index 14832f484..8e69764da 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -100,6 +100,7 @@ cvar_t *sv_enhanced_setplayer; cvar_t *sv_iplimit; cvar_t *sv_status_limit; cvar_t *sv_status_show; +cvar_t *sv_status_ext; cvar_t *sv_uptime; cvar_t *sv_auth_limit; cvar_t *sv_rcon_limit; @@ -523,6 +524,422 @@ static void SVC_Status(void) NET_SendPacket(NS_SERVER, buffer, len, &net_from); } +/* +================ +Extended Status Protocol (statusx) + +Multi-packet extended status response with player statistics +================ +*/ + +#define STATUSX_CHUNK_SIZE 1200 +#define STATUSX_MAX_CHUNKS 32 +#define STATUSX_CACHE_TIME 5000 // 5 seconds + +typedef struct { + char data[STATUSX_CHUNK_SIZE * STATUSX_MAX_CHUNKS]; + size_t total_size; + int chunk_count; + unsigned timestamp; +} statusx_cache_t; + +static statusx_cache_t statusx_cache; + +/* +================ +SV_BuildExtendedStatus + +Builds the complete extended status response with server info, +team data, and detailed player statistics. +Returns the total size written to buffer. +================ +*/ +static size_t SV_BuildExtendedStatus(char *buffer, size_t max_size) +{ + size_t pos = 0; + size_t len; + char entry[1024]; + client_t *cl; + int i, num_clients, num_bots; + + // Count real clients and bots + num_clients = SV_CountClients(); + num_bots = 0; + for (i = 0; i < MAX_CLIENTS; i++) { + if (bot_clients[i].in_use) { + num_bots++; + } + } + + // [SERVER_INFO] + len = Q_scnprintf(entry, sizeof(entry), "[SERVER_INFO]\n"); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + // Server info fields + len = Q_scnprintf(entry, sizeof(entry), "hostname\\%s\n", sv_hostname->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + len = Q_scnprintf(entry, sizeof(entry), "mapname\\%s\n", sv.name); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + len = Q_scnprintf(entry, sizeof(entry), "clients\\%i\n", num_clients); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + len = Q_scnprintf(entry, sizeof(entry), "bots\\%i\n", num_bots); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + len = Q_scnprintf(entry, sizeof(entry), "maxclients\\%i\n", + sv_maxclients->integer - sv_reserved_slots->integer); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + // Game mode and settings from CVAR_SERVERINFO + // Add common game cvars that clients need to know about + cvar_t *gamename = Cvar_FindVar("gamename"); + cvar_t *fraglimit = Cvar_FindVar("fraglimit"); + cvar_t *timelimit = Cvar_FindVar("timelimit"); + cvar_t *dmflags = Cvar_FindVar("dmflags"); + cvar_t *teamplay = Cvar_FindVar("teamplay"); + + if (gamename) { + len = Q_scnprintf(entry, sizeof(entry), "gamename\\%s\n", gamename->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + } + + if (fraglimit) { + len = Q_scnprintf(entry, sizeof(entry), "fraglimit\\%s\n", fraglimit->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + } + + if (timelimit) { + len = Q_scnprintf(entry, sizeof(entry), "timelimit\\%s\n", timelimit->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + } + + if (dmflags) { + len = Q_scnprintf(entry, sizeof(entry), "dmflags\\%s\n", dmflags->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + } + + if (teamplay) { + len = Q_scnprintf(entry, sizeof(entry), "teamplay\\%s\n", teamplay->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + } + + // [PLAYERS] + len = Q_scnprintf(entry, sizeof(entry), "[PLAYERS]\n"); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + // Real clients + FOR_EACH_CLIENT(cl) { + if (cl->state == cs_zombie) { + continue; + } + + int frags = SV_GetClient_Stat(cl, STAT_FRAGS); + int ping = cl->ping; + + len = Q_scnprintf(entry, sizeof(entry), + "%i %i \"%s\"\n", + frags, ping, cl->name); + if (len >= sizeof(entry)) { + continue; + } + if (pos + len >= max_size) { + break; + } + memcpy(buffer + pos, entry, len); + pos += len; + } + + // Bot clients + for (i = 0; i < MAX_CLIENTS; i++) { + if (bot_clients[i].in_use) { + len = Q_scnprintf(entry, sizeof(entry), + "%i %i \"%s\" (BOT)\n", + bot_clients[i].score, bot_clients[i].ping, + bot_clients[i].name); + if (len >= sizeof(entry)) { + continue; + } + if (pos + len >= max_size) { + break; + } + memcpy(buffer + pos, entry, len); + pos += len; + } + } + + // [END] + len = Q_scnprintf(entry, sizeof(entry), "[END]\n"); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + return pos; +} + +/* +================ +SV_SendStatusChunk + +Sends a single chunk of the extended status data. +================ +*/ +static void SV_SendStatusChunk(int chunk_num) +{ + char buffer[MAX_PACKETLEN_DEFAULT]; + size_t len; + size_t chunk_start; + size_t chunk_data_size; + + if (chunk_num < 0 || chunk_num >= statusx_cache.chunk_count) { + return; + } + + chunk_start = chunk_num * STATUSX_CHUNK_SIZE; + chunk_data_size = (chunk_num == statusx_cache.chunk_count - 1) ? + (statusx_cache.total_size - chunk_start) : STATUSX_CHUNK_SIZE; + + len = Q_scnprintf(buffer, sizeof(buffer), + "\xff\xff\xff\xffstatusxdata %d/%d %zu\n", + chunk_num, statusx_cache.chunk_count, chunk_data_size); + + if (len + chunk_data_size >= sizeof(buffer)) { + Com_DPrintf("Chunk %d too large to send\n", chunk_num); + return; + } + + memcpy(buffer + len, statusx_cache.data + chunk_start, chunk_data_size); + len += chunk_data_size; + + NET_SendPacket(NS_SERVER, buffer, len, &net_from); +} + +/* +================ +SVC_StatusExt + +Handles extended status queries (statusx command). +Supports requesting metadata (-1) or specific chunks. +================ +*/ +static void SVC_StatusExt(void) +{ + int chunk_num; + char buffer[MAX_PACKETLEN_DEFAULT]; + size_t len; + unsigned now; + + if (!sv_status_ext->integer) { + return; // Feature disabled + } + + if (SV_RateLimited(&svs.ratelimit_status)) { + Com_DPrintf("Dropping statusx request from %s\n", + NET_AdrToString(&net_from)); + return; + } + + now = Sys_Milliseconds(); + + // Rebuild cache if expired or first time + if (!statusx_cache.chunk_count || (now - statusx_cache.timestamp) > STATUSX_CACHE_TIME) { + statusx_cache.total_size = SV_BuildExtendedStatus(statusx_cache.data, + sizeof(statusx_cache.data)); + statusx_cache.chunk_count = (statusx_cache.total_size + STATUSX_CHUNK_SIZE - 1) / + STATUSX_CHUNK_SIZE; + statusx_cache.timestamp = now; + } + + // Get requested chunk number (default to 0) + chunk_num = (Cmd_Argc() > 1) ? Q_atoi(Cmd_Argv(1)) : 0; + + // Handle metadata request (-1) + if (chunk_num == -1) { + len = Q_scnprintf(buffer, sizeof(buffer), + "\xff\xff\xff\xffstatusxmeta %d %zu\n", + statusx_cache.chunk_count, statusx_cache.total_size); + NET_SendPacket(NS_SERVER, buffer, len, &net_from); + return; + } + + // Send requested chunk + SV_SendStatusChunk(chunk_num); +} + +/* +================ +Extended Rules Protocol (rulesext) + +Multi-packet extended rules response with CVARs marked CVAR_SERVERINFO_EXT +================ +*/ + +#define RULESEXT_CHUNK_SIZE 1200 +#define RULESEXT_MAX_CHUNKS 32 +#define RULESEXT_CACHE_TIME 5000 // 5 seconds + +typedef struct { + char data[RULESEXT_CHUNK_SIZE * RULESEXT_MAX_CHUNKS]; + size_t total_size; + int chunk_count; + unsigned timestamp; +} rulesext_cache_t; + +static rulesext_cache_t rulesext_cache; + +/* +================ +SV_BuildExtendedRules + +Builds the complete extended rules response with all CVARs +marked with CVAR_SERVERINFO_EXT flag. +Returns the total size written to buffer. +================ +*/ +static size_t SV_BuildExtendedRules(char *buffer, size_t max_size) +{ + size_t pos = 0; + size_t len; + char entry[1024]; + cvar_t *cv; + int count = 0; + + // [EXTENDED_RULES] + len = Q_scnprintf(entry, sizeof(entry), "[EXTENDED_RULES]\n"); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + // Add all CVARs marked with CVAR_SERVERINFO_EXT flag + for (cv = cvar_vars; cv; cv = cv->next) { + if (cv->flags & CVAR_SERVERINFO_EXT) { + count++; + len = Q_scnprintf(entry, sizeof(entry), "%s\\%s\n", cv->name, cv->string); + if (len < sizeof(entry) && pos + len < max_size) { + memcpy(buffer + pos, entry, len); + pos += len; + } + } + } + + // [END] + len = Q_scnprintf(entry, sizeof(entry), "[END]\n"); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + return pos; +} + +/* +================ +SV_SendRulesChunk + +Sends a single chunk of the extended rules data. +================ +*/ +static void SV_SendRulesChunk(int chunk_num) +{ + char buffer[MAX_PACKETLEN_DEFAULT]; + size_t len; + size_t chunk_start; + size_t chunk_data_size; + + if (chunk_num < 0 || chunk_num >= rulesext_cache.chunk_count) { + return; + } + + chunk_start = chunk_num * RULESEXT_CHUNK_SIZE; + chunk_data_size = (chunk_num == rulesext_cache.chunk_count - 1) ? + (rulesext_cache.total_size - chunk_start) : RULESEXT_CHUNK_SIZE; + + len = Q_scnprintf(buffer, sizeof(buffer), + "\xff\xff\xff\xffrulesxdata %d/%d %zu\n", + chunk_num, rulesext_cache.chunk_count, chunk_data_size); + + if (len + chunk_data_size >= sizeof(buffer)) { + Com_DPrintf("Rules chunk %d too large to send\n", chunk_num); + return; + } + + memcpy(buffer + len, rulesext_cache.data + chunk_start, chunk_data_size); + NET_SendPacket(NS_SERVER, buffer, len + chunk_data_size, &net_from); +} + +/* +================ +SVC_RulesExt + +Extended rules query handler +================ +*/ +static void SVC_RulesExt(void) +{ + char buffer[MAX_PACKETLEN_DEFAULT]; + int chunk_num = 0; + size_t len; + + if (!sv_status_ext->integer) { + return; + } + + if (SV_RateLimited(&svs.ratelimit_status)) { + Com_DPrintf("Dropping rulesext request from %s\n", + NET_AdrToString(&net_from)); + return; + } + + // Check if we need to rebuild the cache + if (svs.realtime - rulesext_cache.timestamp > RULESEXT_CACHE_TIME) { + len = SV_BuildExtendedRules(rulesext_cache.data, sizeof(rulesext_cache.data)); + rulesext_cache.total_size = len; + rulesext_cache.chunk_count = (len + RULESEXT_CHUNK_SIZE - 1) / RULESEXT_CHUNK_SIZE; + rulesext_cache.timestamp = svs.realtime; + } + + // Parse chunk number from command + if (Cmd_Argc() > 1) { + chunk_num = atoi(Cmd_Argv(1)); + } + + // Handle metadata request + if (chunk_num == -1) { + len = Q_scnprintf(buffer, sizeof(buffer), + "\xff\xff\xff\xffrulesxmeta %d %zu\n", + rulesext_cache.chunk_count, rulesext_cache.total_size); + NET_SendPacket(NS_SERVER, buffer, len, &net_from); + return; + } + + // Send requested chunk + SV_SendRulesChunk(chunk_num); +} + /* ================ SVC_Ack @@ -1448,6 +1865,8 @@ static const ucmd_t svcmds[] = { { "ping", SVC_Ping }, { "ack", SVC_Ack }, { "status", SVC_Status }, + { "statusx", SVC_StatusExt }, + { "rulesx", SVC_RulesExt }, { "info", SVC_Info }, { "getchallenge", SVC_GetChallenge }, { "connect", SVC_DirectConnect }, @@ -2434,6 +2853,8 @@ void SV_Init(void) sv_status_show = Cvar_Get("sv_status_show", "2", 0); + sv_status_ext = Cvar_Get("sv_status_ext", "0", 0); + sv_status_limit = Cvar_Get("sv_status_limit", "15", 0); sv_status_limit->changed = sv_status_limit_changed; diff --git a/src/server/server.h b/src/server/server.h index a7edebffd..19426db91 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -564,6 +564,7 @@ extern cvar_t *sv_enhanced_setplayer; extern cvar_t *sv_status_limit; extern cvar_t *sv_status_show; +extern cvar_t *sv_status_ext; extern cvar_t *sv_auth_limit; extern cvar_t *sv_rcon_limit; extern cvar_t *sv_uptime;