From 776b1921c6de13db1c93f0e55df18a4041898590 Mon Sep 17 00:00:00 2001 From: dusty-qw <71472647+dusty-qw@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:23:44 -0800 Subject: [PATCH 1/8] WIPEOUT: don't skip teaminfo updates for specs --- src/clan_arena.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/clan_arena.c b/src/clan_arena.c index ce7cccb9..9bc7aa5e 100644 --- a/src/clan_arena.c +++ b/src/clan_arena.c @@ -740,11 +740,6 @@ void CA_SendTeamInfo(gedict_t *t) break; } - if (t->ct == ctSpec && t->trackent && (t->trackent == NUM_FOR_EDICT(p))) - { - continue; // if we're spectating the player, don't send info about him - } - if (p->ca_ready || match_in_progress != 2) // be sure to send info if in prewar { if (match_in_progress == 2) From 2f4ef761f76b937d75740a28caf9569dc7d56d13 Mon Sep 17 00:00:00 2001 From: dusty-qw <71472647+dusty-qw@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:42:00 -0800 Subject: [PATCH 2/8] WIPEOUT: update spawn configuration --- src/clan_arena.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/clan_arena.c b/src/clan_arena.c index 9bc7aa5e..217b3ace 100644 --- a/src/clan_arena.c +++ b/src/clan_arena.c @@ -24,13 +24,14 @@ typedef struct wipeout_map_spawns_t // { {coords}, angle, name, radius } static wipeout_spawn_config dm3_spawns[] = { { { -880, -232, -16 }, 90, "tele/sng", 400 }, - { { 192, -208, -176 }, 90, "big>ra", 300 }, + { { 192, -208, -176 }, 90, "ra/mega", 300 }, { { 1472, -928, -24 }, 90, "ya box", 200 }, { { 1520, 432, -88 }, 0, "rl", 400 }, { { -632, -680, -16 }, 90, "tele/ra", 400 }, { { 512, 768, 216 }, -90, "lifts", 300 }, { { -387, 233, -16 }, 0, "sng", 200 }, - { { 2021, -446, -24 }, 180, "bridge", 400 } + { { 1959, -449, -24 }, 180, "bridge", 1000 }, + { { 1846, 690, -180 }, 180, "pent", 2000 } }; static wipeout_map_spawns wipeout_spawn_configs[] = { From a7e1112c76a4b0a2f90be5d7a6d68a52890022d8 Mon Sep 17 00:00:00 2001 From: dusty-qw <71472647+dusty-qw@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:54:22 -0800 Subject: [PATCH 3/8] WIPEOUT: unlimited ammo during endround --- src/clan_arena.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/clan_arena.c b/src/clan_arena.c index 217b3ace..6a1580c5 100644 --- a/src/clan_arena.c +++ b/src/clan_arena.c @@ -1179,6 +1179,21 @@ void CA_OnePlayerStats(gedict_t *p, qbool series_over) p->ca_round_lgfired = a_lg; } +void UnlimitedEndRoundAmmo(void) +{ + gedict_t *p; + int ammo = 9999; + + for (p = world; (p = find_plr(p));) + { + p->s.v.ammo_nails = ammo; + p->s.v.ammo_shells = ammo; + p->s.v.ammo_rockets = ammo; + p->s.v.ammo_cells = ammo; + p->ca_ammo_grenades = ammo; + } +} + void EndRound(int alive_team) { gedict_t *p; @@ -1194,6 +1209,8 @@ void EndRound(int alive_team) loser_respawn_time = loser_team ? team_last_alive_time(loser_team) : 999; } + UnlimitedEndRoundAmmo(); // Surviving players get unlimited ammo during endround phase + pause_count = Q_rint(pause_time - g_globalvars.time); if (pause_count <= 0) @@ -1315,11 +1332,6 @@ void EndRound(int alive_team) print_player_stats(false); team_round_summary(alive_team); } - - if (pause_count < 4) - { - ra_match_fight = 1; // disable firing - } } } From 36353035489ce9cd9b97a0c72d068fc6b604e6b4 Mon Sep 17 00:00:00 2001 From: dusty-qw <71472647+dusty-qw@users.noreply.github.com> Date: Wed, 28 Jan 2026 03:53:54 -0800 Subject: [PATCH 4/8] WIPEOUT: clean up stats tables --- src/clan_arena.c | 207 ++++++++++++++++++++++++++++++++++------------- src/g_utils.c | 7 +- 2 files changed, 156 insertions(+), 58 deletions(-) diff --git a/src/clan_arena.c b/src/clan_arena.c index 6a1580c5..f94b67fe 100644 --- a/src/clan_arena.c +++ b/src/clan_arena.c @@ -4,6 +4,21 @@ #include "g_local.h" +typedef struct ca_player_stats_t +{ + float dmg; // damage given + float dmgt; // damage taken + float frags; // frags + float kil; // kills + float dths; // deaths + float gl_h; // gl hits + float rl_h; // rl hits + float rl_d; // rl directs + float lg_h; // lg hits + float lg_a; // lg attacks + float lg_e; // lg efficiency +} ca_player_stats_t; + typedef struct wipeout_spawn_config_t { vec3_t origin; // spawn point coordinates @@ -49,6 +64,15 @@ static int loser_team; static qbool do_endround_stuff = false; static qbool print_stats = false; static float loser_respawn_time = 999; // number of seconds before a teammate would've respawned +static float best_dmgt; +static float best_dmg; +static float best_score; +static float best_kills; +static float best_deaths; +static float best_gl_hits; +static float best_rl_hits; +static float best_rl_directs; +static float best_lg_eff; void track_player(gedict_t *observer); void enable_player_tracking(gedict_t *e, int follow); @@ -281,6 +305,19 @@ void SM_PrepareCA(void) p->ca_ready = p->ready; p->seconds_to_respawn = 0; p->teamcolor = NULL; + + // must reset stats before new game starts + p->ca_round_frags = 0; + p->ca_round_kills = 0; + p->ca_round_dmg = 0; + p->ca_round_dmgt = 0; + p->ca_round_deaths = 0; + p->ca_round_glhit = 0; + p->ca_round_glfired = 0; + p->ca_round_rlhit = 0; + p->ca_round_rldirect = 0; + p->ca_round_lghit = 0; + p->ca_round_lgfired = 0; } } } @@ -1082,10 +1119,73 @@ void team_round_summary(int alive_team) !alive_team ? "tied round" : (alive_team == 2 ? "round winner" : "")); } +static void CA_GetPlayerStats(gedict_t *p, qbool series_over, ca_player_stats_t *stats) +{ + qbool use_totals = (round_num == 1 || series_over); + + if (use_totals) + { + stats->frags = p->s.v.frags; + stats->dmg = p->ps.dmg_g; + stats->dmgt = p->ps.dmg_t; + stats->kil = stats->frags - ((int)(stats->dmg / 100.0)); + stats->dths = p->deaths; + stats->gl_h = p->ps.wpn[wpGL].vhits; + stats->rl_h = p->ps.wpn[wpRL].vhits; + stats->rl_d = p->ps.wpn[wpRL].hits; + stats->lg_h = p->ps.wpn[wpLG].hits; + stats->lg_a = p->ps.wpn[wpLG].attacks; + stats->lg_e = 100.0 * stats->lg_h / max(1, stats->lg_a); + } + else + { + stats->frags = p->s.v.frags - p->ca_round_frags; + stats->dmg = p->ps.dmg_g - p->ca_round_dmg; + stats->dmgt = p->ps.dmg_t - p->ca_round_dmgt; + stats->kil = stats->frags - ((int)(stats->dmg / 100.0)); + stats->dths = p->deaths - p->ca_round_deaths; + stats->gl_h = p->ps.wpn[wpGL].vhits - p->ca_round_glhit; + stats->rl_h = p->ps.wpn[wpRL].vhits - p->ca_round_rlhit; + stats->rl_d = p->ps.wpn[wpRL].hits - p->ca_round_rldirect; + stats->lg_h = p->ps.wpn[wpLG].hits - p->ca_round_lghit; + stats->lg_a = p->ps.wpn[wpLG].attacks - p->ca_round_lgfired; + stats->lg_e = 100.0 * stats->lg_h / max(1, stats->lg_a); + } +} + +static void CA_UpdateBestStats(gedict_t *p, qbool series_over) +{ + ca_player_stats_t stats; + + CA_GetPlayerStats(p, series_over, &stats); + + best_score = stats.frags > best_score ? stats.frags : best_score; + best_dmg = stats.dmg > best_dmg ? stats.dmg : best_dmg; + best_dmgt = stats.dmgt < best_dmgt ? stats.dmgt : best_dmgt; + best_kills = stats.kil > best_kills ? stats.kil : best_kills; + best_deaths = stats.dths < best_deaths ? stats.dths : best_deaths; + best_gl_hits = stats.gl_h > best_gl_hits ? stats.gl_h : best_gl_hits; + best_rl_hits = stats.rl_h > best_rl_hits ? stats.rl_h : best_rl_hits; + best_rl_directs = stats.rl_d > best_rl_directs ? stats.rl_d : best_rl_directs; + best_lg_eff = stats.lg_e > best_lg_eff ? stats.lg_e : best_lg_eff; +} + void print_player_stats(qbool series_over) { gedict_t *p; + // reset top stats before looping players + // some values set to 1 to avoid displaying 0s as top stats + best_dmgt = 999999.0f; + best_dmg = 0; + best_score = 0; + best_kills = 0; + best_deaths = 999999.0f; + best_gl_hits = 1; + best_rl_hits = 1; + best_rl_directs = 1; + best_lg_eff = 1; + G_bprint(2, "\nsco damg took k d gl rh rd lg%% player\n%s\n", redtext("--- ----- ---- -- -- --- --- --- ---- --------")); @@ -1095,6 +1195,16 @@ void print_player_stats(qbool series_over) if (p->ready && (streq(getteam(p), cvar_string(va("_k_team1"))) || streq(getteam(p), cvar_string(va("_k_team2"))) )) + { + CA_UpdateBestStats(p, series_over); + } + } + + for (p = world; (p = find_plr(p));) + { + if (p->ready && + (streq(getteam(p), cvar_string(va("_k_team1"))) || + streq(getteam(p), cvar_string(va("_k_team2"))) )) { CA_OnePlayerStats(p, series_over); } @@ -1105,54 +1215,39 @@ void print_player_stats(qbool series_over) void CA_OnePlayerStats(gedict_t *p, qbool series_over) { - qbool use_totals = (round_num == 1 || series_over); - float frags; - float rkills; - float dmg_g; - float dmg_t; - float vh_rl; - float h_rl; - float vh_gl; - float h_lg; - float a_lg; - float e_lg; - float round_elg; - char score[10]; - char damage[10]; - char dmg_took[10]; - char kills[10]; - char deaths[10]; - char gl_hits[10]; - char rl_hits[10]; - char rl_directs[10]; - char lg_eff[10]; - - frags = p->s.v.frags; - dmg_g = p->ps.dmg_g; - dmg_t = p->ps.dmg_t; - - rkills = frags - ((int)(dmg_g/100.0)); - h_rl = p->ps.wpn[wpRL].hits; - vh_rl = p->ps.wpn[wpRL].vhits; - vh_gl = p->ps.wpn[wpGL].vhits; - h_lg = p->ps.wpn[wpLG].hits; - a_lg = p->ps.wpn[wpLG].attacks; - e_lg = 100.0 * h_lg / max(1, a_lg); - - if (!use_totals) - { - round_elg = 100 * (h_lg - p->ca_round_lghit) / max(1, a_lg - p->ca_round_lgfired); - } - - snprintf(score, sizeof(score), "%.0f", use_totals ? p->s.v.frags : p->s.v.frags - p->ca_round_frags); - snprintf(damage, sizeof(damage), "%.0f", use_totals ? dmg_g : dmg_g - p->ca_round_dmg); - snprintf(dmg_took, sizeof(dmg_took), "%.0f", use_totals ? dmg_t : dmg_t - p->ca_round_dmgt); - snprintf(kills, sizeof(kills), "%.0f", use_totals ? rkills : rkills - p->ca_round_kills); - snprintf(deaths, sizeof(deaths), "%.0f", use_totals ? p->deaths : p->deaths - p->ca_round_deaths); - snprintf(gl_hits, sizeof(gl_hits), "%.0f", use_totals ? vh_gl : vh_gl - p->ca_round_glhit); - snprintf(rl_hits, sizeof(rl_hits), "%.0f", use_totals ? vh_rl : vh_rl - p->ca_round_rlhit); - snprintf(rl_directs, sizeof(rl_directs), "%.0f", use_totals ? h_rl : h_rl - p->ca_round_rldirect); - snprintf(lg_eff, sizeof(lg_eff), "%.0f", use_totals ? e_lg : round_elg); + ca_player_stats_t stats; + char score[20]; + char damage[20]; + char dmg_took[20]; + char kills[20]; + char deaths[20]; + char gl_hits[20]; + char rl_hits[20]; + char rl_directs[20]; + char lg_eff[20]; + + CA_GetPlayerStats(p, series_over, &stats); + + // debug RL directs (only meaningful for per-round deltas) + // if (!series_over && (stats.rl_d > 10 || stats.rl_d < 0 || p->ps.wpn[wpRL].hits < p->ca_round_rldirect)) + // { + // G_bprint(2, "CA stats anomaly: %s round=%d rl_hits=%d rl_base=%.0f rl_delta=%.0f\n", + // getname(p), round_num, p->ps.wpn[wpRL].hits, p->ca_round_rldirect, stats.rl_d); + // } + + snprintf(score, sizeof(score), "%s", stats.frags == best_score ? dig3(Q_rint(stats.frags)) : dig1s("%.0f", stats.frags)); + snprintf(damage, sizeof(damage), "%s", stats.dmg == best_dmg ? dig3(Q_rint(stats.dmg)) : dig1s("%.0f", stats.dmg)); + snprintf(dmg_took, sizeof(dmg_took), "%s", stats.dmgt == best_dmgt ? dig3(Q_rint(stats.dmgt)) : dig1s("%.0f", stats.dmgt)); + snprintf(kills, sizeof(kills), "%s", stats.kil == best_kills ? dig3(Q_rint(stats.kil)) : dig1s("%.0f", stats.kil)); + snprintf(deaths, sizeof(deaths), "%s", stats.dths == best_deaths ? dig3(Q_rint(stats.dths)) : dig1s("%.0f", stats.dths)); + snprintf(gl_hits, sizeof(gl_hits), "%s", stats.gl_h == best_gl_hits ? dig3(Q_rint(stats.gl_h)) : dig1s("%.0f", stats.gl_h)); + snprintf(rl_hits, sizeof(rl_hits), "%s", stats.rl_h == best_rl_hits ? dig3(Q_rint(stats.rl_h)) : dig1s("%.0f", stats.rl_h)); + snprintf(rl_directs, sizeof(rl_directs),"%s", stats.rl_d == best_rl_directs ? dig3(Q_rint(stats.rl_d)) : dig1s("%.0f", stats.rl_d)); + snprintf(lg_eff, sizeof(lg_eff), "%s", stats.lg_e == best_lg_eff ? dig3(Q_rint(stats.lg_e)) : dig1s("%.0f", stats.lg_e)); + + // debug stats row + // G_cprint("CA stats row: %s frags=%.0f dmg=%.0f dmgt=%.0f kills=%.0f deaths=%.0f gl=%.0f rl=%.0f rd=%.0f lg=%.0f\n", + // getname(p), stats.frags, stats.dmg, stats.dmgt, stats.kil, stats.dths, stats.gl_h, stats.rl_h, stats.rl_d, stats.lg_e); G_bprint(2, "%3s %5s %4s %2s %2s %3s %3s %3s %3s%s %s\n", strneq(score, "0") ? score : "-", @@ -1168,15 +1263,15 @@ void CA_OnePlayerStats(gedict_t *p, qbool series_over) getname(p)); p->ca_round_frags = p->s.v.frags; - p->ca_round_kills = rkills; - p->ca_round_dmg = dmg_g; - p->ca_round_dmgt = dmg_t; + p->ca_round_kills = p->s.v.frags - ((int)(p->ps.dmg_g/100.0)); + p->ca_round_dmg = p->ps.dmg_g; + p->ca_round_dmgt = p->ps.dmg_t; p->ca_round_deaths = p->deaths; - p->ca_round_glhit = vh_gl; - p->ca_round_rlhit = vh_rl; - p->ca_round_rldirect = h_rl; - p->ca_round_lghit = h_lg; - p->ca_round_lgfired = a_lg; + p->ca_round_glhit = p->ps.wpn[wpGL].vhits; + p->ca_round_rlhit = p->ps.wpn[wpRL].vhits; + p->ca_round_rldirect = p->ps.wpn[wpRL].hits; + p->ca_round_lghit = p->ps.wpn[wpLG].hits; + p->ca_round_lgfired = p->ps.wpn[wpLG].attacks; } void UnlimitedEndRoundAmmo(void) diff --git a/src/g_utils.c b/src/g_utils.c index de94b373..8c036831 100644 --- a/src/g_utils.c +++ b/src/g_utils.c @@ -648,8 +648,10 @@ char* dig1(int d) { static char string[MAX_STRINGS][32]; static int index = 0; + + index %= MAX_STRINGS; snprintf(string[index], sizeof(string[0]), "%d", d); - return string[index++ % MAX_STRINGS]; + return string[index++]; } char* dig1s(const char *format, ...) @@ -658,11 +660,12 @@ char* dig1s(const char *format, ...) static int index = 0; va_list argptr; + index %= MAX_STRINGS; va_start(argptr, format); Q_vsnprintf(string[index], sizeof(string[0]), format, argptr); va_end(argptr); - return string[index++ % MAX_STRINGS]; + return string[index++]; } char* dig3(int d) From 8f6c69dd409b81f384a8a4c971fe51b0457c6314 Mon Sep 17 00:00:00 2001 From: dusty-qw <71472647+dusty-qw@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:58:17 -0800 Subject: [PATCH 5/8] WIPEOUT: bubbles --- src/client.c | 2 ++ src/weapons.c | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/client.c b/src/client.c index 2476c1e4..ee6dbb63 100644 --- a/src/client.c +++ b/src/client.c @@ -4707,6 +4707,8 @@ void PlayerPostThink(void) self->client_predflags = PRDFL_FORCEOFF; else if (!CA_can_fire(self)) self->client_predflags = PRDFL_FORCEOFF; + else if ((cvar("k_clan_arena") == 2) && ca_round_pause && self->in_play) + self->client_predflags = PRDFL_FORCEOFF; else if ((match_in_progress == 1) || !can_prewar(true)) self->client_predflags = PRDFL_FORCEOFF; // disable LG prediction in prewar when underwater to avoid weird shit diff --git a/src/weapons.c b/src/weapons.c index f21e6ebe..4ecbf1fe 100644 --- a/src/weapons.c +++ b/src/weapons.c @@ -1839,6 +1839,13 @@ void superspike_touch(void) } else { + // no ricochet or sparks in wipeout endround phase + if (cvar("k_clan_arena") == 2 && ca_round_pause) + { + ent_remove(self); + return; + } + WriteByte( MSG_MULTICAST, SVC_TEMPENTITY); WriteByte( MSG_MULTICAST, TE_SUPERSPIKE); WriteCoord( MSG_MULTICAST, self->s.v.origin[0]); @@ -1853,12 +1860,13 @@ void superspike_touch(void) void W_FireSuperSpikes(void) { vec3_t dir, tmp; + qbool wipe_spike = (cvar("k_clan_arena") == 2) && ca_round_pause && self->in_play; WS_Mark(self, wpSNG); self->ps.wpn[wpSNG].attacks++; - sound(self, CHAN_WEAPON, "weapons/spike2.wav", 1, ATTN_NORM); + sound(self, CHAN_WEAPON, wipe_spike ? "misc/water1.wav" : "weapons/spike2.wav", 1, ATTN_NORM); self->attack_finished = self->client_time + 0.2; if (match_in_progress == 2) @@ -1876,7 +1884,15 @@ void W_FireSuperSpikes(void) tmp[2] += 16; launch_spike(tmp, dir); newmis->touch = (func_t) superspike_touch; - setmodel(newmis, "progs/s_spike.mdl"); + if (wipe_spike) + { + // Wipeout end-round flair for survivor SNG shots. + setmodel(newmis, "progs/s_bubble.spr"); + } + else + { + setmodel(newmis, "progs/s_spike.mdl"); + } setsize(newmis, 0, 0, 0, 0, 0, 0); g_globalvars.msg_entity = EDICT_TO_PROG(self); WriteByte( MSG_ONE, SVC_SMALLKICK); From 1150d7bbd545f8a4adda572cf088b45fd0a5de8d Mon Sep 17 00:00:00 2001 From: blaze Date: Sat, 21 Feb 2026 20:38:16 -0800 Subject: [PATCH 6/8] Bugfix: ability to stop countdown during the last second of it --- src/match.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/match.c b/src/match.c index 4063f9f5..0f74d84a 100644 --- a/src/match.c +++ b/src/match.c @@ -3130,7 +3130,7 @@ void PlayerBreak(void) { p = find(world, FOFCLSN, "timer"); - if (p && p->cnt2 > 1) + if (p && p->cnt2 >= 1) { self->ready = 0; From 4d566b7dc0de2477ddaf7ec18f0a55e909f202c7 Mon Sep 17 00:00:00 2001 From: blaze Date: Sat, 21 Feb 2026 21:12:29 -0800 Subject: [PATCH 7/8] SetChangeParms() no longer persists ready unconditionally. It now stores parm15 only for active match or matchless contexts, otherwise 0. --- src/client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.c b/src/client.c index ee6dbb63..d730c130 100644 --- a/src/client.c +++ b/src/client.c @@ -398,7 +398,7 @@ void SetChangeParms(void) g_globalvars.parm12 = self->k_coach; g_globalvars.parm13 = self->k_stuff; g_globalvars.parm14 = self->ps.handicap; - g_globalvars.parm15 = self->ready; + g_globalvars.parm15 = ((match_in_progress == 2) || cvar("k_matchless")) ? self->ready : 0; } // From 6f35a239584252034845688ce59faba7158955cc Mon Sep 17 00:00:00 2001 From: Ivan Bolsunov Date: Sat, 7 Feb 2026 14:20:08 +0300 Subject: [PATCH 8/8] Fix deathtype field parsing to prevent 64-bit memory overwrite The spawn field table listed "deathtype" as F_LSTRING, so G_ParseField was writing a char* into an int slot. On 64-bit builds this wrote 8 bytes, which corrupted both deathtype (lower 32 bits of the pointer) and the next field (dmgtime, upper 32 bits). This could lead to random deathtype values and clobbered dmgtime, causing incorrect logic and potential crashes. We now ignore the "deathtype" spawn key to avoid writing a pointer into an integer field. --- src/g_spawn.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/g_spawn.c b/src/g_spawn.c index 349d0af9..bb94a60c 100644 --- a/src/g_spawn.c +++ b/src/g_spawn.c @@ -129,7 +129,11 @@ field_t fields[] = { "noise2", FOFS(noise2), F_LSTRING }, { "noise3", FOFS(noise3), F_LSTRING }, { "noise4", FOFS(noise4), F_LSTRING }, - { "deathtype", FOFS(deathtype), F_LSTRING }, + + // KTX use deathtype as a damage type for earch T_Damage and T_RadiusDamage call. + // Vanila QC code uses it as a custom death message type for doors/plats/etc, but we broke it. + { "deathtype", 0, F_IGNORE }, + { "t_length", FOFS(t_length), F_FLOAT }, { "t_width", FOFS(t_width), F_FLOAT }, // TF