Files
fs2open.github.com/code/stats/stats.cpp
T
Goober5000andClaude Opus 4.7 237516c08c Convert CSG/PLR fixed ship/weapon arrays to SCP_vector
Ship_info is already an SCP_vector but MAX_SHIP_CLASSES (500) is held in
place by fixed-size structures throughout the engine and editors. This
converts the seven file-format-adjacent arrays whose persistence touches
the .csg campaign save and .plr pilot file formats:

  campaign::ships_allowed         [MAX_SHIP_CLASSES] -> SCP_vector<ubyte>
  campaign::weapons_allowed       [MAX_WEAPON_TYPES] -> SCP_vector<ubyte>
  loadout_data::ship_pool         [MAX_SHIP_CLASSES] -> SCP_vector<int>
  loadout_data::weapon_pool       [MAX_WEAPON_TYPES] -> SCP_vector<int>
  scoring_struct::kills           [MAX_SHIP_CLASSES] -> SCP_vector<int>
  scoring_struct::m_kills         [MAX_SHIP_CLASSES] -> SCP_vector<int>
  scoring_struct::m_okKills       [MAX_SHIP_CLASSES] -> SCP_vector<int>

The CSG and PLR file formats already store explicit counts and key by
ship/weapon names, so no on-disk format change is needed. Existing
read/write loops already iterated by ship_info_size() / weapon_info_size()
and just compile through against the new vector types.

Vectors are sized to ship_info_size() / weapon_info_size() in
scoring_struct::init(), mission_campaign_clear(), player_loadout_init(),
and the campaign-pool reset paths in csg.cpp and multi_campaign.cpp.
All of these run after weapon_init() / ship_init(), so the sizing
invariant holds. scoring_struct globals constructed at static-init time
start with empty vectors; first index access is always post-table-parse.

Multiplayer stats sync (multimsgs.cpp) now derives offsets and counts
from the vector size; added USHRT_MAX assertions on the sender side
since the wire protocol packs offset/count as USHORT. The FSTracker
sync (multi_fstracker.cpp) clamps vmt->num_ships down to the local
mod's kills.size() before transmission and guards the receive-side
write so a tracker payload with more ship slots than this mod has won't
overrun the vector.

The MAX_SHIP_CLASSES cap itself stays in place at ship.cpp:2327; this
is one prerequisite toward lifting it. The other ~25 fixed-size
ship-class-indexed structures (team_data, Ss_pool_teams, Wl_ships,
FRED2 editor arrays, qtFRED Editor::_ship_usage, etc.) remain to be
addressed in follow-up PRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-30 02:21:22 -04:00

293 lines
7.1 KiB
C++

/*
* Copyright (C) Volition, Inc. 1999. All rights reserved.
*
* All source code herein is the property of Volition, Inc. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
#include "globalincs/systemvars.h"
#include "hud/hud.h"
#include "network/multi.h"
#include "pilotfile/pilotfile.h"
#include "playerman/player.h"
#include "stats/stats.h"
static const scoring_struct *Player_stats;
// coverity[GLOBAL_INIT_ORDER] -- safe; default-constructed, no cross-TU dependencies
static scoring_struct All_time_ever_stats;
void show_stats_init()
{
if (Game_mode & GM_MULTIPLAYER) {
set_player_stats(MY_NET_PLAYER_NUM);
} else {
Player_stats = &Player->stats;
}
// multi only has current stats and all-time-ever stats, so this is only needed in single-player
Pilot.export_stats(&All_time_ever_stats);
}
// write out the label for each stat
void show_stats_label(StatsType type, int sx, int sy, int dy)
{
char text[NAME_LENGTH];
if (type == StatsType::MISSION_STATS)
gr_printf_menu(sx, sy, "%s", XSTR("Mission Stats", 114));
else
gr_printf_menu(sx, sy, "%s", XSTR("All Time Stats", 128));
sy += 2 * dy;
gr_printf_menu(sx, sy, "%s", XSTR("Total kills", 115));
sy += 2 * dy;
gr_printf_menu(sx, sy, "%s", XSTR("Primary weapon shots", 116));
sy += dy;
gr_printf_menu(sx, sy, "%s", XSTR("Primary weapon hits", 117));
sy += dy;
gr_printf_menu(sx, sy, "%s", XSTR("Primary friendly hits", 118));
sy += dy;
strcpy_s(text, XSTR("Primary hit %", 119));
replace_one(text, "%%", "%", NAME_LENGTH - 1);
gr_printf_menu(sx, sy, "%s", text);
sy += dy;
strcpy_s(text, XSTR("Primary friendly hit %", 120));
replace_one(text, "%%", "%", NAME_LENGTH - 1);
gr_printf_menu(sx, sy, "%s", text);
sy += 2 * dy;
gr_printf_menu(sx, sy, "%s", XSTR("Secondary weapon shots", 121));
sy += dy;
gr_printf_menu(sx, sy, "%s", XSTR("Secondary weapon hits", 122));
sy += dy;
gr_printf_menu(sx, sy, "%s", XSTR("Secondary friendly hits", 123));
sy += dy;
strcpy_s(text, XSTR("Secondary hit %", 124));
replace_one(text, "%%", "%", NAME_LENGTH - 1);
gr_printf_menu(sx, sy, "%s", text);
sy += dy;
strcpy_s(text, XSTR("Secondary friendly hit %", 125));
replace_one(text, "%%", "%", NAME_LENGTH - 1);
gr_printf_menu(sx, sy, "%s", text);
sy += 2 * dy;
gr_printf_menu(sx, sy, "%s", XSTR("Assists", 126));
sy += 2 * dy;
if (Game_mode & GM_MULTIPLAYER)
{
if (type == StatsType::MISSION_STATS)
{
gr_printf_menu(sx, sy, "%s", XSTR("Player Deaths", 127));
sy += 2 * dy;
}
if (type == StatsType::MISSION_STATS)
gr_printf_menu(sx, sy, "%s", XSTR("Mission score", 1526));
else
gr_printf_menu(sx, sy, "%s", XSTR("Score", 1527));
}
}
void show_stats_numbers(StatsType type, int sx, int sy, int dy)
{
switch (type)
{
case StatsType::MISSION_STATS:
show_stats_numbers(*Player_stats, true, sx, sy, dy);
break;
case StatsType::ALL_TIME_CAMPAIGN_STATS:
Assertion(!(Game_mode & GM_MULTIPLAYER), "ALL_TIME_CAMPAIGN_STATS are not stored for multiplayer!");
show_stats_numbers(*Player_stats, false, sx, sy, dy);
break;
case StatsType::ALL_TIME_EVER_STATS:
if (Game_mode & GM_MULTIPLAYER)
show_stats_numbers(*Player_stats, false, sx, sy, dy); // in multiplayer, the all-time-ever stats are stored in the player
else
show_stats_numbers(All_time_ever_stats, false, sx, sy, dy);
break;
default:
UNREACHABLE("Unhandled StatsType %d in show_stats_numbers()", static_cast<int>(type));
}
}
int stats_get_kills(StatsType type, int ship_class)
{
switch (type)
{
case StatsType::MISSION_STATS:
return stats_get_kills(*Player_stats, true, ship_class);
case StatsType::ALL_TIME_CAMPAIGN_STATS:
return stats_get_kills(*Player_stats, false, ship_class);
case StatsType::ALL_TIME_EVER_STATS:
if (Game_mode & GM_MULTIPLAYER)
return stats_get_kills(*Player_stats, false, ship_class); // in multiplayer, the all-time-ever stats are stored in the player
else
return stats_get_kills(All_time_ever_stats, false, ship_class);
default:
UNREACHABLE("Unhandled StatsType %d in stats_get_kills()", static_cast<int>(type));
return 0;
}
}
void show_stats_numbers(const scoring_struct &stats, bool use_m_stats, int sx, int sy, int dy)
{
float pct;
int kill_count_ok, p_shots_fired, p_shots_hit, p_bonehead_hits, s_shots_fired, s_shots_hit, s_bonehead_hits, assists, score;
if (use_m_stats)
{
kill_count_ok = stats.m_kill_count_ok;
p_shots_fired = stats.mp_shots_fired;
p_shots_hit = stats.mp_shots_hit;
p_bonehead_hits = stats.mp_bonehead_hits;
s_shots_fired = stats.ms_shots_fired;
s_shots_hit = stats.ms_shots_hit;
s_bonehead_hits = stats.ms_bonehead_hits;
assists = stats.m_assists;
score = stats.m_score;
}
else
{
kill_count_ok = stats.kill_count_ok;
p_shots_fired = stats.p_shots_fired;
p_shots_hit = stats.p_shots_hit;
p_bonehead_hits = stats.p_bonehead_hits;
s_shots_fired = stats.s_shots_fired;
s_shots_hit = stats.s_shots_hit;
s_bonehead_hits = stats.s_bonehead_hits;
assists = stats.assists;
score = stats.score;
}
sy += 2 * dy;
// kills stats
gr_printf_menu(sx, sy, "%d", kill_count_ok);
sy += 2 * dy;
// primary weapon stats
gr_printf_menu(sx, sy, "%u", p_shots_fired);
sy += dy;
gr_printf_menu(sx, sy, "%u", p_shots_hit);
sy += dy;
gr_printf_menu(sx, sy, "%u", p_bonehead_hits);
sy += dy;
if (p_shots_fired > 0)
pct = 100.0f * i2fl(p_shots_hit) / i2fl(p_shots_fired);
else
pct = 0.0f;
gr_printf_menu(sx, sy, "%d%%", fl2i(pct));
sy += dy;
if (p_shots_fired > 0)
pct = 100.0f * i2fl(p_bonehead_hits) / i2fl(p_shots_fired);
else
pct = 0.0f;
gr_printf_menu(sx, sy, "%d%%", fl2i(pct));
sy += 2 * dy;
// secondary weapon stats
gr_printf_menu(sx, sy, "%u", s_shots_fired);
sy += dy;
gr_printf_menu(sx, sy, "%u", s_shots_hit);
sy += dy;
gr_printf_menu(sx, sy, "%u", s_bonehead_hits);
sy += dy;
if (s_shots_fired > 0)
pct = 100.0f * i2fl(s_shots_hit) / i2fl(s_shots_fired);
else
pct = 0.0f;
gr_printf_menu(sx, sy, "%d%%", fl2i(pct));
sy += dy;
if (s_shots_fired > 0)
pct = 100.0f * i2fl(s_bonehead_hits) / i2fl(s_shots_fired);
else
pct = 0.0f;
gr_printf_menu(sx, sy, "%d%%", fl2i(pct));
sy += 2 * dy;
// assists and player rescues (respawns)
gr_printf_menu(sx, sy, "%d", assists);
sy += 2 * dy;
if (Game_mode & GM_MULTIPLAYER)
{
if (use_m_stats)
{
gr_printf_menu(sx, sy, "%d", stats.m_player_deaths);
sy += 2 * dy;
}
// score
gr_printf_menu(sx, sy, "%d", score);
}
}
int stats_get_kills(const scoring_struct &stats, bool use_m_stats, int ship_class)
{
Assertion(stats.kills.in_bounds(ship_class), "ship_class is out of bounds!");
if (use_m_stats)
return stats.m_okKills[ship_class];
else
return stats.kills[ship_class];
}
void show_stats_close()
{
}
// initialize the statistics portion of the player structure for multiplayer. Only the host of
// a netgame needs to be doing this (and if fact, only he *should* be doing this)
void init_multiplayer_stats( )
{
scoring_struct *ptr;
for (int idx=0; idx < MAX_PLAYERS; idx++) {
ptr = &Players[idx].stats;
scoring_level_init( ptr );
}
}
void set_player_stats(int pid)
{
Player_stats = &Net_players[pid].m_player->stats;
}