diff --git a/doc/action.md b/doc/action.md index 97609d603..113a2ccc8 100644 --- a/doc/action.md +++ b/doc/action.md @@ -59,6 +59,8 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team - [Grenade Strength](#grenade-strength) - [Commands](#commands-21) - [Total Kills](#total-kills) + - [Scoreboard Delivery](#scoreboard-delivery) + - [Configurable Scoreboard](#configurable-scoreboard) - [Random Rotation](#random-rotation) - [Commands](#commands-22) - [Vote Rotation](#vote-rotation) @@ -604,6 +606,46 @@ Grenades are a little bit more powerful, so they're not as useless as they were ### Total Kills The scoreboard of TNG will now show the total kills for each player. Kills is the total number of kills without the negatives (suicides, cratering, teamkills) subtracted. +### Scoreboard Delivery + +Scoreboard layout messages are staggered across multiple server frames to prevent packet buffer overflows on servers with many players. Previously, all connected clients received their scoreboard update in a single frame, which could overwhelm the server's outbound message buffers — particularly for legacy clients with small reliable message limits (~1400 bytes). With 32 players this could cause client disconnects or server crashes. + +**Periodic updates**: Each player's scoreboard refreshes every 3 seconds, but individual clients are spread across different frames within that window. A 32-player server sends ~1 scoreboard per frame instead of 32 at once. Team roster changes (`teams_changed`) still trigger an immediate update to all clients. + +**Intermission (end-of-map) scoreboards**: When intermission begins, scoreboard sends are spread across 4 frames (~0.4 seconds) instead of sending all at once. This is imperceptible to players since intermission lasts several seconds. + +**On-demand (TAB key)**: Scoreboard requests from pressing the score key are sent as unreliable messages. If the packet is lost, the periodic 3-second refresh fills it in. This eliminates the risk of dropping a client whose reliable buffer was already full. + +The maximum scoreboard buffer has been increased from 1024 to 1400 bytes, and the maximum players shown per team raised from 8 to 10, taking advantage of the reduced burst pressure from staggering. + +### Configurable Scoreboard + +The `scoreboard` cvar accepts a string of field codes that define which columns appear on the in-game scoreboard (accessed via TAB). Each character maps to a column: + +| Code | Column | Width | +|------|--------|-------| +| `F` | Frags | 5 chars | +| `N` | Player name | 15 chars | +| `M` | Time (minutes) | 4 chars | +| `P` | Ping | 4 chars | +| `S` | Score | 5 chars | +| `K` | Kills | 5 chars | +| `D` | Deaths | 6 chars | +| `I` | Damage (raw) | 6 chars | +| `A` | Accuracy (%) | 3 chars | +| `T` | Team | 4 chars | +| `C` | CTF Caps | 4 chars | + +Default layouts (when `scoreboard` is empty): +- Standard teamplay: `FNMPIT` +- Team deathmatch: `FNMPDT` +- CTF: `SNMPCT` +- No-score mode: `NMP` + +Example: `set scoreboard "FNMPKIT"` replaces frags with kills and adds both raw damage and team columns. + +The layout string sent to clients is constrained to 1400 bytes. Each additional column adds approximately 7-8 bytes per player row. Server operators should be mindful of the total column count when many players are connected — if the string exceeds the limit it will be truncated, which may cut off players at the bottom of the list. + ### Random Rotation Random Map Rotation will make the server pick a random map from the maplist when the current map ends. This will make the rotations less static. diff --git a/src/action/a_team.c b/src/action/a_team.c index c00d9eb7a..4605ee4e2 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -3316,7 +3316,7 @@ int G_NotSortedClients( gclient_t **sortedList ) return total; } -#define MAX_SCOREBOARD_SIZE 1024 +#define MAX_SCOREBOARD_SIZE 1300 #define TEAM_HEADER_WIDTH 160 //skin icon and team tag #define TEAM_ROW_CHARS 32 //"yv 42 string2 \"name\" " #define TEAM_ROW_WIDTH 160 //20 chars, name and possible captain tag @@ -3327,12 +3327,12 @@ int G_NotSortedClients( gclient_t **sortedList ) // Maximum number of lines of scores to put under each team's header. #define MAX_SCORES_PER_TEAM 9 -#define MAX_PLAYERS_PER_TEAM 8 +#define MAX_PLAYERS_PER_TEAM 10 void A_NewScoreboardMessage(edict_t * ent) { - char buf[1024]; - char string[1024] = { '\0' }; + char buf[MAX_SCOREBOARD_SIZE]; + char string[MAX_SCOREBOARD_SIZE] = { '\0' }; gclient_t *sortedClients[MAX_CLIENTS]; int total[TEAM_TOP] = { 0, 0, 0, 0 }; int i, j, line = 0, lineh = 8; diff --git a/src/action/g_local.h b/src/action/g_local.h index 409384ad5..de1de31ae 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2179,6 +2179,7 @@ struct gclient_s int damage_dealt; // total damage dealt to other players (used for hit markers) float killer_yaw; // when dead, look at killer + qboolean needs_intermission_scoreboard; // deferred intermission layout send weaponstate_t weaponstate; vec3_t kick_angles; // weapon kicks diff --git a/src/action/g_main.c b/src/action/g_main.c index 3077f82cc..7430bfc3d 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -828,24 +828,37 @@ void ClientEndServerFrames (void) int i, updateLayout = 0, spectators = 0; edict_t *ent; + // Stagger intermission scoreboard sends across frames if (level.intermission_framenum) { for (i = 0, ent = g_edicts + 1; i < game.maxclients; i++, ent++) { if (!ent->inuse || !ent->client) continue; ClientEndServerFrame(ent); + + if (ent->client->needs_intermission_scoreboard) { + int frames_since = level.realFramenum - level.intermission_framenum; + int my_slot = i % 4; + if (frames_since >= my_slot) { + DeathmatchScoreboardMessage(ent, NULL); +#ifndef NO_BOTS + if (!ent->is_bot) +#endif + gi.unicast(ent, true); + ent->client->needs_intermission_scoreboard = false; + } + } } return; } + // teams_changed forces immediate update for all clients if( teams_changed && FRAMESYNC ) { updateLayout = 1; teams_changed = false; UpdateJoinMenu(); } - else if( !(level.realFramenum % (3 * HZ)) ) - updateLayout = 1; // calc the player views now that all pushing // and damage has been added @@ -856,7 +869,12 @@ void ClientEndServerFrames (void) ClientEndServerFrame(ent); - if (updateLayout && ent->client->layout) { + // Stagger periodic layout updates: each client on a different frame + // within the 3-second cycle, unless forced by teams_changed + int clientUpdate = updateLayout || + ((level.realFramenum % (3 * HZ)) == (i % (3 * HZ))); + + if (clientUpdate && ent->client->layout) { if (ent->client->layout == LAYOUT_MENU) PMenu_Update(ent); else @@ -871,7 +889,8 @@ void ClientEndServerFrames (void) spectators++; } - if (updateLayout && spectators && spectator_hud->value >= 0) { + int updateSpectators = updateLayout || !(level.realFramenum % (3 * HZ)); + if (updateSpectators && spectators && spectator_hud->value >= 0) { G_UpdateSpectatorStatusbar(); if (level.spec_statusbar_lastupdate >= level.realFramenum - 3 * HZ) { diff --git a/src/action/p_hud.c b/src/action/p_hud.c index ae752ceb4..8631e7906 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -132,9 +132,8 @@ void MoveClientToIntermission(edict_t *ent) if( ent->is_bot ) return; #endif - // add the layout - DeathmatchScoreboardMessage(ent, NULL); - gi.unicast(ent, true); + // Defer the layout send to ClientEndServerFrames for staggered delivery + ent->client->needs_intermission_scoreboard = true; } void BeginIntermission(edict_t *targ) @@ -415,7 +414,7 @@ void DeathmatchScoreboard(edict_t *ent) return; #endif DeathmatchScoreboardMessage(ent, ent->enemy); - gi.unicast(ent, true); + gi.unicast(ent, false); // unreliable; periodic refresh catches drops }