From 69925b77f1b235c632721d18ddf303f5beb810c7 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/9] 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 816ec822..7f22d554 100644 --- a/src/clan_arena.c +++ b/src/clan_arena.c @@ -738,11 +738,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 871f70cbad30584b85c189484d7cdac505155ace 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/9] 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 7f22d554..7ce90720 100644 --- a/src/clan_arena.c +++ b/src/clan_arena.c @@ -22,13 +22,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 cd7f2eedcfdbc435e1a386fb30aacc4d8b375de8 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/9] 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 7ce90720..aec3e42a 100644 --- a/src/clan_arena.c +++ b/src/clan_arena.c @@ -1177,6 +1177,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; @@ -1192,6 +1207,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) @@ -1313,11 +1330,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 759d094198ee0a7c30c355f4c7b94f809b3ac5bd 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/9] 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 aec3e42a..71ffd939 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 @@ -47,6 +62,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); @@ -279,6 +303,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; } } } @@ -1080,10 +1117,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("--- ----- ---- -- -- --- --- --- ---- --------")); @@ -1093,6 +1193,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); } @@ -1103,54 +1213,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 : "-", @@ -1166,15 +1261,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 a3f696e2..aa8eaaf4 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 a929d8b3174498b7f4393a30c809056d54bfc47c 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/9] WIPEOUT: bubbles --- src/weapons.c | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/weapons.c b/src/weapons.c index f4ace981..e607dd69 100644 --- a/src/weapons.c +++ b/src/weapons.c @@ -1641,6 +1641,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]); @@ -1655,12 +1662,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 = g_globalvars.time + 0.2; if (match_in_progress == 2) @@ -1678,7 +1686,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 fae8b5638d1e2e0b41e7c3d292ad38394194cea8 Mon Sep 17 00:00:00 2001 From: blaze Date: Sat, 21 Feb 2026 20:38:16 -0800 Subject: [PATCH 6/9] 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 99c8b061..6d3bff1a 100644 --- a/src/match.c +++ b/src/match.c @@ -3115,7 +3115,7 @@ void PlayerBreak(void) { p = find(world, FOFCLSN, "timer"); - if (p && p->cnt2 > 1) + if (p && p->cnt2 >= 1) { self->ready = 0; From 56cbc389bdf6af4acc6a70eb1e11ac80d8be8682 Mon Sep 17 00:00:00 2001 From: blaze Date: Sat, 21 Feb 2026 21:12:29 -0800 Subject: [PATCH 7/9] 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 c4f72ae9..f4af2c7a 100644 --- a/src/client.c +++ b/src/client.c @@ -397,7 +397,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 50d32d0211060a1ba83723de1a596f3e53d5c9d6 Mon Sep 17 00:00:00 2001 From: dusty-qw <71472647+dusty-qw@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:24:34 -0800 Subject: [PATCH 8/9] Merge pull request #1 from b1az3/master Bugfix: ability to stop countdown during the last second From 7afcf752e3183eaaafc5a08cf5ea185f9910d90b Mon Sep 17 00:00:00 2001 From: Ivan Bolsunov Date: Sat, 7 Feb 2026 14:20:08 +0300 Subject: [PATCH 9/9] 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