diff --git a/README.md b/README.md index 8ad78ce9..10e33d02 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ FortressOne Server New features ------ -* `setinfo precise_grenades on/off` to enable precise timing when throwing grenades. This removes a random, up to, 100ms input delay. (default on) +* `localinfo project_grenades 0/1` [default: 0] Adjust the point at which + grenades are primed to correct for client latency. Does not allow players to +throw grenades any faster, or more frequently; only more consistently. +* Server option to limit `sv_maxclients` to current number of players during quad gametime. `localinfo limit_quad_players 0/1`. Default: `1`. * `localinfo forcereload 0/1` Option to prevent forced reloads. * `+grenade1` and `+grenade2` grenade buttons (more reliable than impulses), push to prime, again to throw. * `+dropflag` Allows player to hold button and flag will be thrown on contact. diff --git a/csqc/csdefs.qc b/csqc/csdefs.qc index 37b5e5aa..ff7db300 100644 --- a/csqc/csdefs.qc +++ b/csqc/csdefs.qc @@ -241,8 +241,6 @@ const float CHAN_WEAPON = 1; const float CHAN_VOICE = 2; const float CHAN_ITEM = 3; const float CHAN_BODY = 4; -const float CHAN_GREN1 = 5; -const float CHAN_GREN2 = 6; const float ATTN_NONE = 0; /* Sounds with this attenuation can be heard throughout the map */ const float ATTN_NORM = 1; /* Standard attenuation */ const float ATTN_IDLE = 2; /* Extra attenuation so that sounds don't travel too far. */ diff --git a/csqc/csextradefs.qc b/csqc/csextradefs.qc index 1c5e9550..5a2b0b1a 100644 --- a/csqc/csextradefs.qc +++ b/csqc/csextradefs.qc @@ -436,14 +436,77 @@ typedef struct { } FO_Hud_Settings; FO_Hud_Settings HudSettings; -typedef struct { - float grentype; - float expires; - float icon_index; - float alpha; -} FO_Hud_Grentimer; +enumflags { + FL_GT_SOUND, + FL_GT_THROWN, + FL_GT_ADJPING +}; -FO_Hud_Grentimer FO_Hud_Grentimers[5]; +class CsGrenTimer; +CsGrenTimer grentimers[NUM_GREN_TIMERS]; + +class CsGrenTimer { + float index_; + float primed_at_; + float expires_at_; + float flags_; + float grentype_; + + nonvirtual float() index { return index_; }; + nonvirtual float() active { return expires_at_ > time; }; + nonvirtual float() expiry { return expires_at_; }; + nonvirtual float() grentype { return grentype_; }; + nonvirtual void() set_thrown { flags_ |= FL_GT_THROWN; }; + nonvirtual float() is_thrown { return flags_ & FL_GT_THROWN; }; + nonvirtual float() sound_offset { + float offset = max(3.8 - (expires_at_ - time), 0); + return offset; + }; + + nonvirtual void(float primed_at, float expires_at, + float grentype, float timer_flags) Set; + nonvirtual void() Stop; + + nonvirtual void() PauseSound { + if (flags_ & FL_GT_SOUND) // -volume => stop. + soundupdate(this, CHAN_VOICE, "", -1, 0, 0, 0, 0); + }; + nonvirtual void() StartSound; + + static float next_index_; + static CsGrenTimer last_; + + static void() Init { + for (float i = 0; i < grentimers.length; i++) + grentimers[i] = spawn(CsGrenTimer, index_: i); + }; + + static void() StopAll { + for (float i = 0; i < grentimers.length; i++) + grentimers[i].Stop(); + }; + + static CsGrenTimer() GetNext { + next_index_ = (next_index_ + 1) % grentimers.length; + last_ = grentimers[next_index_]; + return last_; + }; + static void() SyncPause { + // Time froze, but sound kept running, realign. + for (float i = 0; i < grentimers.length; i++) + if (grentimers[i].active) + grentimers[i].PauseSound(); + }; + + static void() SyncUnpause { + // Time froze, but sound kept running, realign. + for (float i = 0; i < grentimers.length; i++) + if (grentimers[i].active) + grentimers[i].StartSound(); + }; + + static CsGrenTimer() GetLast { return last_; }; +}; // scoreboard stuff typedef struct { @@ -542,6 +605,7 @@ float grentimer_waiting; float jumpsound_cooldown; float last_pmove_onground; float last_vel_z; +float last_death_time; vector FO_Hud_Icon_Size = [24, 24, 0]; vector FO_Hud_Icon_Font_Size = [8, 8, 0]; diff --git a/csqc/csprogs.src b/csqc/csprogs.src index 2aa77962..f61b3f0a 100644 --- a/csqc/csprogs.src +++ b/csqc/csprogs.src @@ -11,6 +11,7 @@ csextradefs.qc ../share/commondefs.qc ../share/common_helpers.qc ../share/common_vote.qc +settings.qc sui_sys.qc vote.qc status.qc @@ -19,6 +20,5 @@ main.qc events.qc hud_helpers.qc hud.qc -settings.qc input.qc #endlist diff --git a/csqc/events.qc b/csqc/events.qc index b68a0390..61822a4f 100644 --- a/csqc/events.qc +++ b/csqc/events.qc @@ -1,10 +1,6 @@ void ParseSBAR(); -void ParseGrenPrimed(float grentype, float timertype); -void AddGrenTimer(float grentype, float offset); -void AddSilentGrenTimer(float grentype, float offset); -void StartGrenTimer(float slot, float grentype, float offset); -void AddGrenTimer(float grentype, float offset); -void PlayGrenTimer(float offset); +void ParseGrenPrimed(float grentype, float primed_at, float explodes_at); +float StartGrenTimer(float primed_at, float expires_at, float grentype, float play_sound); void() CSQC_Parse_Event = { float msgtype = readbyte(); @@ -104,14 +100,17 @@ void() CSQC_Parse_Event = { SBAR.Identify = readstring(); break; case MSG_GRENPRIMED: - float grentype = readfloat(); - float timertype = cvar(FOCMD_GRENTIMER); - ParseGrenPrimed(grentype, timertype); + float grentype = readbyte(); + float primed_at = readfloat(); + float explodes_at = readfloat(); + ParseGrenPrimed(grentype, primed_at, explodes_at); break; case MSG_GRENTHROWN: - ApplyTransparencyToGrenTimer(); + CsGrenTimer last = CsGrenTimer::GetLast(); + last.set_thrown(); break; case MSG_PLAYERDIE: + last_death_time = readfloat(); StopGrenTimers(); break; case MSG_CLIENT_MENU: @@ -302,48 +301,98 @@ void() CSQC_Parse_Event = { case MSG_VOTE_MAP_DELETE: RemoveVoteMap(readstring(), FALSE); break; + case MSG_PAUSE: + CsGrenTimer::SyncPause(); + break; + case MSG_UNPAUSE: + CsGrenTimer::SyncUnpause(); + break; } } -void PlayGrenTimer(float offset) { - local float channel = CHAN_GREN1; - local float soundtime; - local float highest_soundtime = -1; +DEFCVAR_FLOAT(fo_grentimer_debug, 0); - for(float i = CHAN_GREN1; i <= CHAN_GREN5; i++) { - soundtime = getsoundtime(self, i); +string cached_timer; +string GetGrenTimerSound() { + string wav = CVARS(FOCMD_GRENTIMERSOUND); - if (soundtime < 0) { - channel = i; - break; - } + if (cached_timer != wav) { + precache_sound(wav); + cached_timer = wav; + } + return wav; +} + +void CsGrenTimer::StartSound() { + if !(this.flags_ & FL_GT_SOUND) + return; + + string wav = GetGrenTimerSound(); + float volume = CVARF(FOCMD_GRENTIMERVOLUME); + + // Note there's a bug where soundupdate returns false for a new sample, even + // though it's started. + soundupdate(this, CHAN_VOICE, wav, volume, 0, 0, 0, sound_offset()); +} + + +void CsGrenTimer::Set(float primed_at, float expires_at, float _grentype, + float timer_flags) { + grentype_ = _grentype; + primed_at_ = primed_at; + expires_at_ = expires_at; + flags_ = timer_flags; - if (soundtime > highest_soundtime) { - highest_soundtime = soundtime; - channel = i; - } + if (this.flags_ & FL_GT_ADJPING) { + local float rtt = + getplayerkeyfloat(player_localnum, INFOKEY_P_PING) / 1000; + expires_at_ -= rtt/2; } - local string sound = cvar_string(FOCMD_GRENTIMERSOUND); - localsound(sound, channel, cvar(FOCMD_GRENTIMERVOLUME)); // required because soundupdate doesn't reset getsoundtime - soundupdate(self, channel, sound, cvar(FOCMD_GRENTIMERVOLUME), 0, 0, 0, offset); + StartSound(); } -void ParseGrenPrimed(float grentype, float timertype) { - if(timertype == 0) { - AddSilentGrenTimer(grentype, 0); +void CsGrenTimer::Stop() { + expires_at_ = -1; + PauseSound(); // Pause is really stop. + flags_ = 0; +} + +void StopGrenTimers() { + // New style. + for (float i = 0; i < NUM_GREN_TIMERS; i++) + grentimers[i].Stop(); +} + +void ParseGrenPrimed(float grentype, float primed_at, float explodes_at) { + if (grentype == GR_TYPE_FLARE || grentype == GR_TYPE_CALTROP) return; - } - if(timertype == 1) { - AddGrenTimer(grentype, 0); + if (primed_at < last_death_time) return; + + local float timer_flags = 0; + switch (CVARF(FOCMD_GRENTIMER)) { + case 0: break; + case 1: timer_flags = FL_GT_SOUND; break; + // 2 [and something sane for anything we don't recognize.] + default: timer_flags = FL_GT_SOUND | FL_GT_ADJPING; break; } - if(timertype == 2) { - local float offset = getplayerkeyfloat(player_localnum, INFOKEY_P_PING)/1000; - AddGrenTimer(grentype, offset); - return; + float debug_print_state = CVARF(fo_grentimer_debug) & 1; + + CsGrenTimer timer = CsGrenTimer::GetNext(); + timer.Set(primed_at, explodes_at, grentype, timer_flags); + + if (debug_print_state) { + float ping = getplayerkeyfloat(player_localnum, INFOKEY_P_PING) / 1000; + + float expires_old = time + 3.8 - ping; + float expires_new = timer.expiry(); + float expires_ideal = explodes_at - ping/2; + + print(sprintf("primed_at=%0.2f explodes_at=%0.2f fuse=%0.2f\n", + primed_at, explodes_at, explodes_at - primed_at)); } } diff --git a/csqc/hud.qc b/csqc/hud.qc index 6245acc8..4dea948e 100644 --- a/csqc/hud.qc +++ b/csqc/hud.qc @@ -660,7 +660,7 @@ void Hud_DrawFlagStatusBar(string panelid) { if (FlagInfoLines[i].id) { - alpha = FlagInfoLines[i].state == FLAGINFO_HOME ? cvar("fo_hud_idle_alpha") : 1; + alpha = FlagInfoLines[i].state == FLAGINFO_HOME ? CVARF(fo_hud_idle_alpha) : 1; string icon = FlagInfoLines[i].icon.filename; vector iconcolour = FlagInfoLines[i].icon.colour; float bigfont = 8 * (Hud_Panels[HUD_PANEL_FLAGINFO].TextScale ? Hud_Panels[HUD_PANEL_FLAGINFO].TextScale : Hud_Panels[HUD_PANEL_FLAGINFO].Scale); diff --git a/csqc/hud_helpers.qc b/csqc/hud_helpers.qc index 93724fc0..385af80d 100644 --- a/csqc/hud_helpers.qc +++ b/csqc/hud_helpers.qc @@ -230,20 +230,6 @@ void Hud_DrawStringLMPThreshold(vector pos, string value, float size, float thre } } -string FormatCfgString(string line, string field, string value) -{ - line = strcat(line, "\n", field, ":", value); - - return line; -} - -string FormatCfgVector(string line, string field, vector value) -{ - line = strcat(line, "\n", field, ":", strcat(ftos(value_x), ",", ftos(value_y))); - - return line; -} - string GetPanelString(string line, string name) { line = FormatCfgVector(line, strcat(name, ".position"), DrawPanel.Position); @@ -483,32 +469,3 @@ vector DrawOffsetString(string msg, vector size, vector fontSize, vector pos, ve return pos; } - -void AddGrenTimer(float grentype, float offset) { - if (grentype == GR_TYPE_FLARE || grentype == GR_TYPE_CALTROP) - return; - - for(float i = 0; i < FO_Hud_Grentimers.length; i++) { - if(FO_Hud_Grentimers[i].grentype == 0) { - StartGrenTimer(i, grentype, offset); - PlayGrenTimer(offset); - break; - } - } -} - -void AddSilentGrenTimer(float grentype, float offset) { - for(float i = 0; i < FO_Hud_Grentimers.length; i++) { - if(FO_Hud_Grentimers[i].grentype == 0) { - StartGrenTimer(i, grentype, offset); - break; - } - } -} - -void StartGrenTimer(float slot, float grentype, float offset) { - FO_Hud_Grentimers[slot].grentype = grentype; - FO_Hud_Grentimers[slot].icon_index = grentype; - FO_Hud_Grentimers[slot].alpha = 1; - FO_Hud_Grentimers[slot].expires = time + 3.8 - offset; -} diff --git a/csqc/main.qc b/csqc/main.qc index f76a10c1..2f24e093 100644 --- a/csqc/main.qc +++ b/csqc/main.qc @@ -10,7 +10,6 @@ void Hud_WriteCfg(string path); void FO_LoadSettings(); void FO_WriteSettings(); void AddGrenTimer(float grentype, float offset); -void ApplyTransparencyToGrenTimer(); void StopGrenTimers(); float IsValidToUseGrenades(); void Sync_GameState(); @@ -28,6 +27,8 @@ noref void(float apiver, string enginename, float enginever) CSQC_Init = { // precache_pic(HudIcons[i].icon); // } + CsGrenTimer::Init(); + registercommand("fo_hud_editor"); registercommand("fo_hud_reload"); registercommand("fo_hud_reset"); @@ -35,11 +36,6 @@ noref void(float apiver, string enginename, float enginever) CSQC_Init = { registercommand("fo_hud_save"); registercommand("fo_hud_load"); - registercvar(FOCMD_GRENTIMER, "1"); - registercvar(FOCMD_GRENTIMERSOUND, "grentimer.wav"); - registercvar(FOCMD_GRENTIMERVOLUME, "1"); - registercvar(FOCMD_JUMPVOLUME, "1"); - registercommand("fo_menu_game"); registercommand("fo_main_menu"); registercommand("fo_menu_team"); @@ -51,7 +47,6 @@ noref void(float apiver, string enginename, float enginever) CSQC_Init = { registercommand("fo_menu_build"); registercommand("fo_menu_dropammo"); registercommand("fo_menu_cancel"); - registercvar(FOCMD_ADMIN_MENU_UPDATE_TIME, "2"); registercommand("+aux_jump"); registercommand("-aux_jump"); @@ -81,7 +76,6 @@ noref void(float apiver, string enginename, float enginever) CSQC_Init = { registercommand("vote_addmap"); registercommand("vote_removemap"); - registercvar("fo_hud_idle_alpha", "0.3"); for(float i = 0; i < MENU_OPTION.length - 1; i++) { registercvar(strcat("fo_menu_option_",MENU_OPTION[i]), MENU_OPTION[i]); } @@ -142,7 +136,7 @@ noref void(float width, float height, float menushown) CSQC_UpdateView = { if ((input_buttons & BUTTON2) && (pmove_vel_z < 180 /* not sliding */)) { if (jumpsound_cooldown < time) { jumpsound_cooldown = time + 0.01; - localsound("player/plyrjmp8.wav", CHAN_BODY, cvar(FOCMD_JUMPVOLUME)); + localsound("player/plyrjmp8.wav", CHAN_BODY, CVARF(FOCMD_JUMPVOLUME)); } } @@ -241,7 +235,7 @@ noref float(string cmd) CSQC_ConsoleCommand = { Menu_Cancel(); break; case "+fo_showscores": - if (cvar(FOCMD_OLDSCOREBOARD) == 1) + if (CVARF(FOCMD_OLDSCOREBOARD) == 1) { tokenize(findkeysforcommand(argv(0))); @@ -270,7 +264,7 @@ noref float(string cmd) CSQC_ConsoleCommand = { case "+showscores": case "+showteamscores": showingscores = TRUE; - if (cvar(FOCMD_OLDSCOREBOARD) != 1) + if (CVARF(FOCMD_OLDSCOREBOARD) != 1) { tokenize(findkeysforcommand(argv(0))); @@ -448,29 +442,6 @@ void CSQC_Shutdown() = { FO_WriteSettings(); } -void ApplyTransparencyToGrenTimer() { - for(float i = 0; i < FO_Hud_Grentimers.length; i++) { - if (FO_Hud_Grentimers[i].grentype != 0 && FO_Hud_Grentimers[i].alpha != 0.3) { - FO_Hud_Grentimers[i].alpha = 0.3; - break; - } - } -} - -void StopGrenTimers() { - soundupdate(self, CHAN_GREN1, "", 0, 0, 0, 0, 0); - soundupdate(self, CHAN_GREN2, "", 0, 0, 0, 0, 0); - soundupdate(self, CHAN_GREN3, "", 0, 0, 0, 0, 0); - soundupdate(self, CHAN_GREN4, "", 0, 0, 0, 0, 0); - soundupdate(self, CHAN_GREN5, "", 0, 0, 0, 0, 0); - - for(float i = 0; i < FO_Hud_Grentimers.length; i++) { - FO_Hud_Grentimers[i].grentype = 0; - FO_Hud_Grentimers[i].expires = 0; - FO_Hud_Grentimers[i].icon_index = 0; - } -} - float last_servercommandframe; void _Sync_ServerCommandFrame() { // Server command frames are monotonically unique, we can skip processing diff --git a/csqc/menu.qc b/csqc/menu.qc index 46adcd35..0ca07f8f 100644 --- a/csqc/menu.qc +++ b/csqc/menu.qc @@ -371,7 +371,7 @@ var fo_menu FO_MENU_DISPENSER_USE = { void updateAdminMenuInfo() = { if(admin_menu_next_update < time) { localcmd("cmd adminrefresh\n"); - admin_menu_next_update = time + cvar(FOCMD_ADMIN_MENU_UPDATE_TIME); + admin_menu_next_update = time + CVARF(FOCMD_ADMIN_MENU_UPDATE_TIME); } } diff --git a/csqc/settings.qc b/csqc/settings.qc index cdf3d3fd..88034d11 100644 --- a/csqc/settings.qc +++ b/csqc/settings.qc @@ -1,24 +1,50 @@ +// Saved/Loaded/Restored by Settings funcs +DEFCVAR_FLOAT(FOCMD_GRENTIMER, 2); // Sound + Ping adjust +DEFCVAR_STRING(FOCMD_GRENTIMERSOUND, "grentimer.wav"); +DEFCVAR_FLOAT(FOCMD_GRENTIMERVOLUME, 1); +DEFCVAR_FLOAT(FOCMD_JUMPVOLUME, 1); +DEFCVAR_FLOAT(FOCMD_OLDSCOREBOARD, 0); + +// CVARS that just pass via regular config state. +DEFCVAR_FLOAT(fo_hud_idle_alpha, 0.3); +DEFCVAR_FLOAT(FOCMD_ADMIN_MENU_UPDATE_TIME, 2); + +string FormatCfgString(string line, string field, string value) +{ + line = strcat(line, "\n", field, ":", value); + + return line; +} + +string FormatCfgVector(string line, string field, vector value) +{ + line = strcat(line, "\n", field, ":", strcat(ftos(value_x), ",", ftos(value_y))); + + return line; +} + + void FO_WriteSettings() { // this overwrites float filehandle; filehandle = fopen(FO_CONFIG_PATH, FILE_WRITE); - string line = FormatCfgString("", FOCMD_GRENTIMER, ftos(cvar(FOCMD_GRENTIMER))); - line = FormatCfgString(line, FOCMD_GRENTIMERSOUND, cvar_string(FOCMD_GRENTIMERSOUND)); - line = FormatCfgString(line, FOCMD_GRENTIMERVOLUME, cvar_string(FOCMD_GRENTIMERVOLUME)); - line = FormatCfgString(line, FOCMD_JUMPVOLUME, cvar_string(FOCMD_JUMPVOLUME)); - line = FormatCfgString(line, FOCMD_OLDSCOREBOARD, ftos(cvar(FOCMD_OLDSCOREBOARD))); + string line = FormatCfgString("", FOCMD_GRENTIMER, ftos(CVARF(FOCMD_GRENTIMER))); + line = FormatCfgString(line, FOCMD_GRENTIMERSOUND, CVARS(FOCMD_GRENTIMERSOUND)); + line = FormatCfgString(line, FOCMD_GRENTIMERVOLUME, ftos(CVARF(FOCMD_GRENTIMERVOLUME))); + line = FormatCfgString(line, FOCMD_JUMPVOLUME, ftos(CVARF(FOCMD_JUMPVOLUME))); + line = FormatCfgString(line, FOCMD_OLDSCOREBOARD, ftos(CVARF(FOCMD_OLDSCOREBOARD))); fputs(filehandle, line); fclose(filehandle); } void FO_LoadDefaultSettings() { - cvar_set(FOCMD_GRENTIMER, "1"); - cvar_set(FOCMD_GRENTIMERSOUND, "grentimer.wav"); - cvar_set(FOCMD_GRENTIMERVOLUME, "1"); - cvar_set(FOCMD_JUMPVOLUME, "1"); - cvar_set(FOCMD_OLDSCOREBOARD, "0"); + CVARF(FOCMD_GRENTIMER) = 2; + CVARS(FOCMD_GRENTIMERSOUND) = "grentimer.wav"; + CVARF(FOCMD_GRENTIMERVOLUME) = 1; + CVARF(FOCMD_JUMPVOLUME) = 1; + CVARF(FOCMD_OLDSCOREBOARD) = 0; } void FO_LoadSettings() @@ -48,19 +74,19 @@ void FO_LoadSettings() switch(field) { case FOCMD_GRENTIMER: - cvar_set(FOCMD_GRENTIMER, val); + CVARF(FOCMD_GRENTIMER) = stof(val); break; case FOCMD_GRENTIMERSOUND: - cvar_set(FOCMD_GRENTIMERSOUND, val); + CVARS(FOCMD_GRENTIMERSOUND) = val; break; case FOCMD_GRENTIMERVOLUME: - cvar_set(FOCMD_GRENTIMERVOLUME, val); + CVARF(FOCMD_GRENTIMERVOLUME) = stof(val); break; case FOCMD_JUMPVOLUME: - cvar_set(FOCMD_JUMPVOLUME, val); + CVARF(FOCMD_JUMPVOLUME) = stof(val); break; case FOCMD_OLDSCOREBOARD: - cvar_set(FOCMD_OLDSCOREBOARD, val); + CVARF(FOCMD_OLDSCOREBOARD) = stof(val); break; } } diff --git a/csqc/status.qc b/csqc/status.qc index b099dc3e..7e9128af 100644 --- a/csqc/status.qc +++ b/csqc/status.qc @@ -96,12 +96,11 @@ void(string panelid, float display, string text) drawSpecial = { }; void(string panelid, float display, string text) drawGrenTimerPanel = { - local float timeleft; - local float timercount = 0; + float timeleft; + float timercount = 0; if (display) { FO_Hud_Panel* panel = getAnyHudPanelByNamePointer(panelid); - float Scale = panel.Scale; float TextScale = panel.TextScale; FO_Hud_Panel panel2; @@ -111,35 +110,34 @@ void(string panelid, float display, string text) drawGrenTimerPanel = { panel2.Display = panel.Display; panel2.FillSize = panel.FillSize; - for(float i = 0; i < FO_Hud_Grentimers.length; i++) { - if(FO_Hud_Grentimers[i].grentype > 0) { - timeleft = floor(FO_Hud_Grentimers[i].expires - time) + 1; - if(timeleft < 1) { - //Expire and fill the gap if there are any active grentimers afterwards - FO_Hud_Grentimers[i].grentype = 0; - for(float j = i + 1; j < FO_Hud_Grentimers.length; j++) { - FO_Hud_Grentimers[j - 1].grentype = FO_Hud_Grentimers[j].grentype; - FO_Hud_Grentimers[j - 1].expires = FO_Hud_Grentimers[j].expires; - FO_Hud_Grentimers[j - 1].icon_index = FO_Hud_Grentimers[j].icon_index; - FO_Hud_Grentimers[j - 1].alpha = FO_Hud_Grentimers[j].alpha; - } - FO_Hud_Grentimers[FO_Hud_Grentimers.length - 1].grentype = 0; - FO_Hud_Grentimers[FO_Hud_Grentimers.length - 1].expires = 0; - FO_Hud_Grentimers[FO_Hud_Grentimers.length - 1].icon_index = 0; - FO_Hud_Grentimers[FO_Hud_Grentimers.length - 1].alpha = 0; - continue; - } + CsGrenTimer lt = CsGrenTimer::GetLast(); + // We want to render in reverse mod order from the last primed grenade + // here, that way we'll properly stack timers relative to expiry. + for (float i = grentimers.length; i > 0; i--) { + CsGrenTimer gt = grentimers[(lt.index() + i) % grentimers.length]; - if(i) { - panel2.Scale = Scale * 0.8; - panel2.TextScale = TextScale * 0.8; - } + if (!gt.active()) + continue; - Hud_DrawPanelLMP(&panel2, ftos(timeleft), GrenadeIcons[FO_Hud_Grentimers[i].icon_index].icon, FO_Hud_Grentimers[i].alpha); + timeleft = floor(gt.expiry() - time) + 1; + if (timeleft < 1) + continue; - panel2.Position = panel2.Position + [0, panel2.FillSize.y * panel2.Scale]; - timercount++; + if (timercount) { + panel2.Scale = Scale * 0.8; + panel2.TextScale = TextScale * 0.8; } + + // It's technically possible to get a primed message before thrown + // if things get reordered, but since we can only ever have one + // grenade primed, we can obscure this by always considering + // grenades after the first as primed. + float alpha = (gt.is_thrown() || timercount) ? 0.3 : 1.0; + Hud_DrawPanelLMP(&panel2, ftos(timeleft), + GrenadeIcons[gt.grentype()].icon, alpha); + panel2.Position = panel2.Position + + [0, panel2.FillSize.y * panel2.Scale]; + timercount++; } } if(fo_hud_editor && (!display || !timercount)) { diff --git a/share/commondefs.qc b/share/commondefs.qc index 71bd23ff..aa8eb494 100644 --- a/share/commondefs.qc +++ b/share/commondefs.qc @@ -32,6 +32,8 @@ #define MSG_VOTE_MAP_ADD 20 #define MSG_VOTE_MAP_DELETE 21 #define MSG_PLAYERDIE 22 +#define MSG_PAUSE 23 +#define MSG_UNPAUSE 24 #define FLAGINFO_HOME 1 #define FLAGINFO_CARRIED 2 diff --git a/share/defs.h b/share/defs.h index 89e3a0c3..c7f48f09 100644 --- a/share/defs.h +++ b/share/defs.h @@ -173,11 +173,12 @@ #define CHAN_ITEM 3 #define CHAN_BODY 4 #define CHAN_NO_PHS_ADD 8 -#define CHAN_GREN1 9 -#define CHAN_GREN2 10 -#define CHAN_GREN3 11 -#define CHAN_GREN4 12 -#define CHAN_GREN5 13 + +// We can overlap these with regular channels since they play on the world ent. +#define NUM_GREN_TIMERS 5 // We overlap channels when >3 active. +#define CHAN_GREN_START 9 +#define CHAN_GREN_END 13 +// #define CHAN_GREN_END (CHAN_GREN_START + NUM_GREN_TIMERS - 1) #define ATTN_NONE 0 #define ATTN_NORM 1 diff --git a/ssqc/admin.qc b/ssqc/admin.qc index bd56df78..8f3ab367 100644 --- a/ssqc/admin.qc +++ b/ssqc/admin.qc @@ -187,9 +187,25 @@ void () Admin_CeaseFire = { } }; +void NotifyPauseUnpause(float is_pause) { + entity p = find (world, classname, "player"); + while (p) { + if (p.netname == "" || !infokeyf(p, INFOKEY_P_CSQCACTIVE)) + continue; + + msg_entity = p; + WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); + WriteByte(MSG_MULTICAST, is_pause ? MSG_PAUSE : MSG_UNPAUSE); + multicast('0 0 0', MULTICAST_ONE_R_NOSPECS); + + p = find(p, classname, "player"); + } +} + void () Admin_Pause = { if (!is_paused) { setpause(1); + NotifyPauseUnpause(TRUE); is_paused = 1; cs_paused = 1; pause_actor = self.netname; diff --git a/ssqc/antilag.qc b/ssqc/antilag.qc index c55baec6..e82405dd 100644 --- a/ssqc/antilag.qc +++ b/ssqc/antilag.qc @@ -30,6 +30,23 @@ void AL_ProjectProjectile (entity projectile) { setorigin(projectile, projected_origin); } +float AL_GrenadeOffset() { + if (!project_grenades) + return 0; + + local float offset = infokeyf(self, INFOKEY_P_PING) / 1000; + local float smallest_grenade_think = 0.5; + + offset /= 2; + // Make sure we'll always order before first think. + offset = min(offset, smallest_grenade_think - 0.1); + + // Don't allow prime to move before throw (or to the same frame). + offset = min(offset, time - (self.last_throw + 0.013)); + + return offset; +} + #define DEBUG_ANTILAG 0 #if DEBUG_ANTILAG @@ -360,4 +377,3 @@ void UnrewindPlayer(entity p) { FOPlayer fop = (FOPlayer)p; fop.RestoreNow(); } - diff --git a/ssqc/client.qc b/ssqc/client.qc index c8f84dc5..2c091a70 100644 --- a/ssqc/client.qc +++ b/ssqc/client.qc @@ -622,9 +622,12 @@ void () DecodeLevelParms = { // project projectile weapons by player ping project_weapons = CF_GetSetting("pw", "project_weapons", "off"); - // max projection latency (default 100ms) + // max projectile projection latency (default 100ms) project_weapons_max_latency = CF_GetSetting("pwml", "project_weapons_max_latency", ftos(0.1)); + // Correct grenade prime timing by player ping. + project_grenades = CF_GetSetting("pg", "project_grenades", "on"); + // delay respawning by this many seconds [0] Role_None.respawn_delay_time = CF_GetSetting("rd", "respawn_delay", "0"); if (Role_None.respawn_delay_time) { @@ -1019,6 +1022,7 @@ void () DecodeLevelParms = { medicaura = FALSE; medicnocuss = FALSE; project_weapons = FALSE; + project_grenades = TRUE; distance_based_cuss_duration = FALSE; pyro_type = 1; drop_grenades = FALSE; @@ -1491,7 +1495,7 @@ void () spawn_guard_think = { if(!te.has_disconnected && infokey(te, "dap") == "1") { oldself = self; self = te; - TeamFortress_PrimeGrenade(1); + TeamFortress_PrimeGrenade(1, FALSE); self = oldself; } te = find (te, classname, "player"); diff --git a/ssqc/mvdsv.qc b/ssqc/mvdsv.qc index 2930774e..d77e8df5 100644 --- a/ssqc/mvdsv.qc +++ b/ssqc/mvdsv.qc @@ -107,6 +107,7 @@ void (float duration) GE_PausedTic = { local entity p; if (unpause_requested && (unpause_countdown == 0)) { unpause_countdown = duration + 5; + unpause_lastcountnumber = 5; } if (unpause_requested) { if ((duration >= unpause_countdown)) { @@ -116,56 +117,20 @@ void (float duration) GE_PausedTic = { unpause_requested = 0; unpause_lastcountnumber = 0; setpause(0); - } - else { - if (duration >= (unpause_countdown - 4) && (unpause_lastcountnumber == 0)) { - unpause_lastcountnumber = 4; - p = find (world, classname, "player"); - while (p) { - if (p.netname != "") { - stuffcmd(p, "play buttons/switch04.wav\n"); - } - p = find(p, classname, "player"); - } - bprint(PRINT_HIGH, "Unpausing in 4 seconds\n" ); - - } - else if (duration >= (unpause_countdown - 3) && (unpause_lastcountnumber == 4)) { - unpause_lastcountnumber = 3; - p = find (world, classname, "player"); - while (p) { - if (p.netname != "") { - stuffcmd(p, "play buttons/switch04.wav\n"); - } - p = find(p, classname, "player"); - } - bprint(PRINT_HIGH, "Unpausing in 3 seconds\n" ); - } - else if (duration >= (unpause_countdown - 2) && (unpause_lastcountnumber == 3)) { - unpause_lastcountnumber = 2; - p = find (world, classname, "player"); - while (p) { - if (p.netname != "") { - stuffcmd(p, "play buttons/switch04.wav\n"); - } - p = find(p, classname, "player"); - } - bprint(PRINT_HIGH, "Unpausing in 2 seconds\n" ); - } - else if (duration >= (unpause_countdown - 1) && (unpause_lastcountnumber == 2)) { - unpause_lastcountnumber = 1; - p = find (world, classname, "player"); - while (p) { - if (p.netname != "") { - stuffcmd(p, "play buttons/switch04.wav\n"); - } - p = find(p, classname, "player"); - } - bprint(PRINT_HIGH, "Unpausing in 1 second\n" ); + NotifyPauseUnpause(FALSE); + } else if (duration >= unpause_countdown - (unpause_lastcountnumber)) { + unpause_lastcountnumber--; + p = find (world, classname, "player"); + while (p) { + if (p.netname != "") + stuffcmd(p, "play buttons/switch04.wav\n"); + p = find(p, classname, "player"); } + bprint(PRINT_HIGH, + sprintf("Unpausing in %d seconds\n", unpause_lastcountnumber)); } } -} +} void(float pauseduration) SV_PausedTic = { GE_PausedTic(pauseduration); diff --git a/ssqc/qw.qc b/ssqc/qw.qc index 269b0b3b..ad1b9e26 100644 --- a/ssqc/qw.qc +++ b/ssqc/qw.qc @@ -599,6 +599,7 @@ float max_armor_hwguy; float project_weapons; float project_weapons_max_latency; +float project_grenades; float ng_velocity; float ng_damage; @@ -763,6 +764,7 @@ float FO_FlashDimension; // precise grenades .entity grenade_timer; +.float last_throw; // allow for default goal state .float default_group_no; @@ -785,9 +787,10 @@ string goal_class_names[7] = { // people complain about settings, let's track them -string settings_to_track_list[2] = { +string settings_to_track_list[3] = { "sv_antilag", - "project_weapons" + "project_weapons", + "project_grenades", }; typedef struct setting_t { diff --git a/ssqc/status.qc b/ssqc/status.qc index d5a47aff..f23d22cc 100644 --- a/ssqc/status.qc +++ b/ssqc/status.qc @@ -796,13 +796,15 @@ string GetSBClassInfo(entity pl, float csqcactive) return st1; } -void UpdateClientGrenadePrimed(entity pl, float grentype) = { +void UpdateClientGrenadePrimed(entity pl, float grentype, float explodes_at) = { if(!infokeyf(pl, INFOKEY_P_CSQCACTIVE)) return; msg_entity = pl; WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); WriteByte(MSG_MULTICAST, MSG_GRENPRIMED); - WriteFloat(MSG_MULTICAST, grentype); + WriteByte(MSG_MULTICAST, grentype); + WriteFloat(MSG_MULTICAST, time); + WriteFloat(MSG_MULTICAST, explodes_at); multicast('0 0 0', MULTICAST_ONE_R_NOSPECS); } @@ -821,7 +823,8 @@ void UpdateClientPlayerDie(entity pl) = { msg_entity = pl; WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); WriteByte(MSG_MULTICAST, MSG_PLAYERDIE); - multicast('0 0 0', MULTICAST_ONE_NOSPECS); + WriteFloat(MSG_MULTICAST, time); + multicast('0 0 0', MULTICAST_ONE_R_NOSPECS); } void UpdateClientIDString(entity pl) { diff --git a/ssqc/tfort.qc b/ssqc/tfort.qc index 63b2ee6a..d5e69479 100644 --- a/ssqc/tfort.qc +++ b/ssqc/tfort.qc @@ -790,6 +790,7 @@ void () GrenadeTimer = { void (entity timer) FO_SpawnThrownGrenade = { local entity user = timer.owner; user.tfstate &= ~(TFSTATE_GRENPRIMED | TFSTATE_GRENTHROWING); + user.last_throw = time; if (grentimers && infokeyf(user, INFOKEY_P_CSQCACTIVE)) { UpdateClientGrenadeThrown(user); @@ -923,7 +924,7 @@ void (float inp) TeamFortress_PrimeThrowGrenade = { (self.tfstate & TFSTATE_GRENTHROWING)) TeamFortress_ThrowGrenade(); else { - TeamFortress_PrimeGrenade(inp); + TeamFortress_PrimeGrenade(inp, TRUE); if ( ((inp == 1 && self.tp_grenade_switch != 1) || (inp == 2 && self.tp_grenade_switch == 1)) && self.tp_grenades_1 == GR_TYPE_CALTROP) @@ -931,11 +932,11 @@ void (float inp) TeamFortress_PrimeThrowGrenade = { } }; -void () TeamFortress_GrenadePrimedThink; -void () FO_PreciseGrenadeThink; -float () FO_UsePreciseGrenades; +void () FO_GrenadeThink; -void (float inp) TeamFortress_PrimeGrenade = { +// is_player defines whether this originated from the player or server. Player +// generated primes are corrected for latency when project_grenades is on. +void (float inp, float is_player) TeamFortress_PrimeGrenade = { local float gtype; local string gs; local string ptime; @@ -1098,12 +1099,14 @@ void (float inp) TeamFortress_PrimeGrenade = { tGrenade.impulse = TF_GRENADE_1; else if (inp == 2) tGrenade.impulse = TF_GRENADE_2; - tGrenade.nextthink = time + 0.8; + local float lag_adjust = is_player ? AL_GrenadeOffset() : 0; + + tGrenade.nextthink = time + 0.8 - lag_adjust; if (gtype == GR_TYPE_CALTROP) - tGrenade.heat = time + 0.5 + 0.5; + tGrenade.heat = time + 0.5 + 0.5 - lag_adjust; else { - tGrenade.heat = time + 3 + 0.8; + tGrenade.heat = time + 3 + 0.8 - lag_adjust; RemoveGrenadeTimers(); @@ -1111,7 +1114,7 @@ void (float inp) TeamFortress_PrimeGrenade = { local float notimers = stof(infokey(self, "nt")); if (grentimers && notimers != 1) { timer = spawn(); - timer.nextthink = time + 0.8; + timer.nextthink = time + 0.8 - lag_adjust; timer.think = GrenadeTimer; timer.heat = 4; timer.owner = self; @@ -1121,43 +1124,16 @@ void (float inp) TeamFortress_PrimeGrenade = { } } if (grentimers && csqcactive) { - UpdateClientGrenadePrimed(self, gtype); + UpdateClientGrenadePrimed(self, gtype, tGrenade.heat); } } - if (FO_UsePreciseGrenades()) - tGrenade.think = FO_PreciseGrenadeThink; - else - tGrenade.think = TeamFortress_GrenadePrimedThink; + tGrenade.think = FO_GrenadeThink; self.grenade_timer = tGrenade; }; -void () TeamFortress_GrenadePrimedThink = { - local entity user; - - user = self.owner; - if (!(user.tfstate & TFSTATE_GRENTHROWING) && !user.deadflag && !user.has_disconnected) { - self.nextthink = time + 0.1; - if (!self.think) - dremove(self); - - if (time > self.heat && self.weapon != GR_TYPE_CALTROP) - TeamFortress_ExplodePerson(); - return; - } - if (!(user.tfstate & TFSTATE_GRENPRIMED)) - dprint("GrenadePrimed logic error\n"); - - FO_SpawnThrownGrenade(self); - dremove(self); -}; - -float () FO_UsePreciseGrenades = { - return FO_GetUserSetting(self, "precise_grenades", "pg", "on"); -} - -void () FO_PreciseGrenadeThink = { +void () FO_GrenadeThink = { local entity user = self.owner; // Claim: These cases do not exist for FO; to be removed once proven true. @@ -1181,12 +1157,13 @@ void () FO_PreciseGrenadeThink = { self.nextthink = self.heat; } } else { - TeamFortress_ExplodePerson(); + if (self.weapon != GR_TYPE_CALTROP) + TeamFortress_ExplodePerson(); dremove(self); } } -void () FO_ThrowPreciseGrenade = { +void () FO_ThrowGrenade = { local entity timer = self.grenade_timer; if (timer.nextthink != timer.heat || timer.heat - time < 0.1) { // We do not allow throwing within the first second, or the last 0.1. @@ -1207,10 +1184,7 @@ void () TeamFortress_ThrowGrenade = { return; self.tfstate |= TFSTATE_GRENTHROWING; - // While this is controlled by localinfo, it's a per-grenade value. - if (FO_UsePreciseGrenades() && - self.grenade_timer.think == FO_PreciseGrenadeThink) - FO_ThrowPreciseGrenade(); + FO_ThrowGrenade(); }; void () TeamFortress_GrenadeSwitch = { diff --git a/ssqc/weapons.qc b/ssqc/weapons.qc index 544eccd2..6939a302 100644 --- a/ssqc/weapons.qc +++ b/ssqc/weapons.qc @@ -30,7 +30,7 @@ void () TeamFortress_ShowTF; void () TeamFortress_AssaultWeapon; void () TeamFortress_IncendiaryCannon; void () TeamFortress_FlameThrower; -void (float inp) TeamFortress_PrimeGrenade; +void (float inp, float is_player) TeamFortress_PrimeGrenade; void () TeamFortress_ThrowGrenade; void (float inp) TeamFortress_PrimeThrowGrenade; void () TeamFortress_GrenadeSwitch; @@ -3025,9 +3025,9 @@ void () ImpulseCommands = { if (!self.is_feigning) { if (self.impulse == TF_GRENADE_1) - TeamFortress_PrimeGrenade(1); + TeamFortress_PrimeGrenade(1, TRUE); else if (self.impulse == TF_GRENADE_2) - TeamFortress_PrimeGrenade(2); + TeamFortress_PrimeGrenade(2, TRUE); else if (self.impulse == TF_GRENADE_T) TeamFortress_ThrowGrenade(); else if (self.impulse == TF_GRENADE_PT_1) @@ -3515,7 +3515,7 @@ void () ButtonFrame = { // +grenade1 keydown frame if (keydowns & BUTTON5) { if (hold_grens) { - TeamFortress_PrimeGrenade(1); + TeamFortress_PrimeGrenade(1, TRUE); } else { TeamFortress_PrimeThrowGrenade(1); } @@ -3531,7 +3531,7 @@ void () ButtonFrame = { // +grenade2 keydown frame if (keydowns & BUTTON6) { if (hold_grens) { - TeamFortress_PrimeGrenade(2); + TeamFortress_PrimeGrenade(2, TRUE); } else { TeamFortress_PrimeThrowGrenade(2); }