Files
fs2open.github.com/code/pilotfile/csg.cpp
T
Goober5000andClaude Opus 4.8 bed228d264 avoid undefined behavior from UNREACHABLE on reachable code paths (#7555)
* avoid undefined behavior from UNREACHABLE on reachable code paths

UNREACHABLE expands to __assume(false) / __builtin_unreachable() in
release builds, which is undefined behavior if the code is ever actually
reached. Many UNREACHABLE sites only guard against unexpected-but-
possible runtime states (values from tables, mission/save files, network
packets, scripting, or user input), so reaching them should degrade
gracefully rather than invoke UB.

Replace UNREACHABLE with Assertion(false, ...) at all such sites. It
still pops an error in debug builds, but in release it is a harmless
no-op that simply falls through to the following code. Sites that are
genuinely unreachable by local invariants (e.g. a switch over a bounded
random number) are left as UNREACHABLE.

A few converted sites had no safe fall-through -- an uninitialized read,
null dereference, or null call would have followed -- so they also get a
minimal recovery: initialize the value, guard the dereference, return
early, or skip the offending item.

Where the converted site was a simple guard of the form
    if (x) { Assertion(false, ...); return; }
hoist the check onto the invariant instead:
    Assertion(!x, ...);
    if (x) { return; }
This states the expectation directly and shows the offending value in
the debug popup, while keeping the same graceful runtime degradation.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* addendum

After consultation with Lafiel, use UNREACHABLE in situations that are effectively impossible even if not provably impossible.  UNREACHABLE = "this shouldn't ever happen"; Assertion = "if this ever happens, it's a coding error".  Also fix a few other sites and change a couple to Warnings.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-09 11:20:25 +09:00

2051 lines
55 KiB
C++

#include "controlconfig/controlsconfig.h"
#include "controlconfig/presets.h"
#include "cutscene/cutscenes.h"
#include "freespace.h"
#include "gamesnd/eventmusic.h"
#include "hud/hudconfig.h"
#include "io/joy.h"
#include "io/mouse.h"
#include "menuui/techmenu.h"
#include "mission/missioncampaign.h"
#include "mission/missionload.h"
#include "missionui/missionscreencommon.h"
#include "missionui/missionshipchoice.h"
#include "options/OptionsManager.h"
#include "parse/sexp_container.h"
#include "pilotfile/pilotfile.h"
#include "pilotfile/plr_hudprefs.h"
#include "playerman/player.h"
#include "ship/ship.h"
#include "sound/audiostr.h"
#include "stats/medals.h"
#include "utils/string_utils.h"
#include "weapon/weapon.h"
#define REDALERT_INTERNAL
#include "missionui/redalert.h"
#include <iostream>
#include <sstream>
#include <limits>
// this is kinda tricky
enum class TechroomState : ubyte
{
DEFAULT = 0,
ADDED = 1,
REMOVED = 2
};
void pilotfile::csg_read_flags()
{
// tips?
p->tips = (int)cfread_ubyte(cfp);
// avoid having to read everything to get the rank
if (csg_ver >= 5) {
p->stats.rank = cfread_int(cfp);
}
}
void pilotfile::csg_write_flags()
{
startSection(Section::Flags);
// tips
cfwrite_ubyte((ubyte)p->tips, cfp);
// avoid having to read everything to get the rank
cfwrite_int(p->stats.rank, cfp);
endSection();
}
void pilotfile::csg_read_info()
{
char t_string[NAME_LENGTH+1] = { '\0' };
index_list_t ilist;
int idx, list_size = 0;
ubyte allowed = 0;
if ( !m_have_flags ) {
throw "Info before Flags!";
}
//
// NOTE: lists may contain missing/invalid entries for current data
// this is not necessarily fatal
//
// ship list (NOTE: may contain more than MAX_SHIP_CLASSES)
list_size = cfread_int(cfp);
for (idx = 0; idx < list_size; idx++) {
cfread_string_len(t_string, NAME_LENGTH, cfp);
ilist.name = t_string;
ilist.index = ship_info_lookup(t_string);
ship_list.push_back(ilist);
}
// weapon list (NOTE: may contain more than MAX_WEAPON_TYPES)
list_size = cfread_int(cfp);
for (idx = 0; idx < list_size; idx++) {
cfread_string_len(t_string, NAME_LENGTH, cfp);
ilist.name = t_string;
ilist.index = weapon_info_lookup(t_string);
weapon_list.push_back(ilist);
}
// intel list (NOTE: may contain more than MAX_INTEL_ENTRIES)
list_size = cfread_int(cfp);
for (idx = 0; idx < list_size; idx++) {
cfread_string_len(t_string, NAME_LENGTH, cfp);
ilist.name = t_string;
ilist.index = intel_info_lookup(t_string);
intel_list.push_back(ilist);
}
// medals list (NOTE: may contain more than Num_medals)
list_size = cfread_int(cfp);
for (idx = 0; idx < list_size; idx++) {
cfread_string_len(t_string, NAME_LENGTH, cfp);
ilist.name = t_string;
ilist.index = medals_info_lookup(t_string);
medals_list.push_back(ilist);
}
// last ship flown (index into ship_list)
idx = cfread_int(cfp);
// check the idx is within bounds
Assertion ((idx < (int)ship_list.size()), "Campaign file contains an incorrect value for the last flown ship class. No data in ship_list for ship number %d.", idx);
if (idx >= (int)ship_list.size())
idx = -1;
else if (idx != -1)
p->last_ship_flown_si_index = ship_list[idx].index;
else
p->last_ship_flown_si_index = -1;
// progression state
Campaign.prev_mission = cfread_int(cfp);
Campaign.next_mission = cfread_int(cfp);
// check that the next mission won't be greater than the total number of missions
// though ensure we only flag if campaign exists and has been loaded
if (Campaign.num_missions > 0 && Campaign.next_mission >= Campaign.num_missions) {
Campaign.next_mission = 0; // Prevent trying to load from invalid mission data downstream
m_data_invalid = true; // Causes a warning popup to be displayed
}
// loop state
Campaign.loop_reentry = cfread_int(cfp);
Campaign.loop_enabled = cfread_int(cfp);
// missions completed
Campaign.num_missions_completed = cfread_int(cfp);
// allowed ships
list_size = (int)ship_list.size();
for (idx = 0; idx < list_size; idx++) {
allowed = cfread_ubyte(cfp);
if (allowed) {
if (ship_list[idx].index >= 0) {
Campaign.ships_allowed[ship_list[idx].index] = 1;
} else {
mprintf(("Found invalid ship \"%s\" in campaign save file. Skipping...\n", ship_list[idx].name.c_str()));
}
}
}
// allowed weapons
list_size = (int)weapon_list.size();
for (idx = 0; idx < list_size; idx++) {
allowed = cfread_ubyte(cfp);
if (allowed) {
if (weapon_list[idx].index >= 0) {
Campaign.weapons_allowed[weapon_list[idx].index] = 1;
} else {
mprintf(("Found invalid weapon \"%s\" in campaign save file. Skipping...\n",
weapon_list[idx].name.c_str()));
}
}
}
if (csg_ver >= 2) {
// single/campaign squad name & image
cfread_string_len(p->s_squad_name, NAME_LENGTH, cfp);
cfread_string_len(p->s_squad_filename, MAX_FILENAME_LEN, cfp);
}
// if anything we need/use was missing then it should be considered fatal
if (m_data_invalid) {
throw "Invalid data for CSG!";
}
}
void pilotfile::csg_write_info()
{
int idx;
startSection(Section::Info);
// ship list
cfwrite_int(ship_info_size(), cfp);
for (auto &si : Ship_info) {
cfwrite_string_len(si.name, cfp);
}
// weapon list
cfwrite_int(weapon_info_size(), cfp);
for (auto &wi : Weapon_info) {
cfwrite_string_len(wi.name, cfp);
}
// intel list
cfwrite_int(intel_info_size(), cfp);
for (auto &ii : Intel_info) {
cfwrite_string_len(ii.name, cfp);
}
// medals list
cfwrite_int((int)Medals.size(), cfp);
for (idx = 0; idx < (int)Medals.size(); idx++) {
cfwrite_string_len(Medals[idx].name, cfp);
}
// last ship flown
cfwrite_int(p->last_ship_flown_si_index, cfp);
// progression state
cfwrite_int(Campaign.prev_mission, cfp);
cfwrite_int(Campaign.next_mission, cfp);
// loop state
cfwrite_int(Campaign.loop_reentry, cfp);
cfwrite_int(Campaign.loop_enabled, cfp);
// missions completed
cfwrite_int(Campaign.num_missions_completed, cfp);
// allowed ships
for (idx = 0; idx < ship_info_size(); idx++) {
cfwrite_ubyte(Campaign.ships_allowed[idx], cfp);
}
// allowed weapons
for (idx = 0; idx < weapon_info_size(); idx++) {
cfwrite_ubyte(Campaign.weapons_allowed[idx], cfp);
}
// single/campaign squad name & image
cfwrite_string_len(p->s_squad_name, cfp);
cfwrite_string_len(p->s_squad_filename, cfp);
endSection();
}
void pilotfile::csg_read_missions()
{
int i, j, idx, list_size;
cmission *missionp;
if ( !m_have_info ) {
throw "Missions before Info!";
}
for (i = 0; i < Campaign.num_missions_completed; i++) {
idx = cfread_int(cfp);
missionp = &Campaign.missions[idx];
missionp->completed = 1;
// flags
missionp->flags = cfread_int(cfp);
// goals
missionp->goals.clear();
int num_goals = cfread_int(cfp);
for (j = 0; j < num_goals; j++) {
missionp->goals.emplace_back();
auto& stored_goal = missionp->goals.back();
cfread_string_len(stored_goal.name, NAME_LENGTH, cfp);
stored_goal.status = cfread_char(cfp);
}
// events
missionp->events.clear();
int num_events = cfread_int(cfp);
for (j = 0; j < num_events; j++) {
missionp->events.emplace_back();
auto& stored_event = missionp->events.back();
cfread_string_len(stored_event.name, NAME_LENGTH, cfp);
stored_event.status = cfread_char(cfp);
}
// variables
missionp->variables.clear();
int num_variables = cfread_int(cfp);
for (j = 0; j < num_variables; j++) {
missionp->variables.emplace_back();
auto& stored_variable = missionp->variables.back();
stored_variable.type = cfread_int(cfp);
cfread_string_len(missionp->variables[j].text, TOKEN_LENGTH, cfp);
cfread_string_len(missionp->variables[j].variable_name, TOKEN_LENGTH, cfp);
}
// scoring stats
missionp->stats.score = cfread_int(cfp);
missionp->stats.rank = cfread_int(cfp);
missionp->stats.assists = cfread_int(cfp);
missionp->stats.kill_count = cfread_int(cfp);
missionp->stats.kill_count_ok = cfread_int(cfp);
missionp->stats.bonehead_kills = cfread_int(cfp);
missionp->stats.p_shots_fired = cfread_uint(cfp);
missionp->stats.p_shots_hit = cfread_uint(cfp);
missionp->stats.p_bonehead_hits = cfread_uint(cfp);
missionp->stats.s_shots_fired = cfread_uint(cfp);
missionp->stats.s_shots_hit = cfread_uint(cfp);
missionp->stats.s_bonehead_hits = cfread_uint(cfp);
// ship kills (scoring)
list_size = (int)ship_list.size();
for (j = 0; j < list_size; j++) {
idx = cfread_int(cfp);
if (ship_list[j].index >= 0) {
missionp->stats.kills[ship_list[j].index] = idx;
}
}
// medals (scoring)
list_size = (int)medals_list.size();
for (j = 0; j < list_size; j++) {
idx = cfread_int(cfp);
if (medals_list[j].index >= 0) {
missionp->stats.medal_counts[medals_list[j].index] = idx;
}
}
}
}
void pilotfile::csg_write_missions()
{
int idx, j;
cmission *missionp;
startSection(Section::Missions);
for (idx = 0; idx < MAX_CAMPAIGN_MISSIONS; idx++) {
if (Campaign.missions[idx].completed) {
missionp = &Campaign.missions[idx];
cfwrite_int(idx, cfp);
// flags
cfwrite_int(missionp->flags, cfp);
// goals
cfwrite_int((int)missionp->goals.size(), cfp);
for (j = 0; j < (int)missionp->goals.size(); j++) {
cfwrite_string_len(missionp->goals[j].name, cfp);
cfwrite_char(missionp->goals[j].status, cfp);
}
// events
cfwrite_int((int)missionp->events.size(), cfp);
for (j = 0; j < (int)missionp->events.size(); j++) {
cfwrite_string_len(missionp->events[j].name, cfp);
cfwrite_char(missionp->events[j].status, cfp);
}
// variables
cfwrite_int((int)missionp->variables.size(), cfp);
for (j = 0; j < (int)missionp->variables.size(); j++) {
cfwrite_int(missionp->variables[j].type, cfp);
cfwrite_string_len(missionp->variables[j].text, cfp);
cfwrite_string_len(missionp->variables[j].variable_name, cfp);
}
// scoring stats
cfwrite_int(missionp->stats.score, cfp);
cfwrite_int(missionp->stats.rank, cfp);
cfwrite_int(missionp->stats.assists, cfp);
cfwrite_int(missionp->stats.kill_count, cfp);
cfwrite_int(missionp->stats.kill_count_ok, cfp);
cfwrite_int(missionp->stats.bonehead_kills, cfp);
cfwrite_uint(missionp->stats.p_shots_fired, cfp);
cfwrite_uint(missionp->stats.p_shots_hit, cfp);
cfwrite_uint(missionp->stats.p_bonehead_hits, cfp);
cfwrite_uint(missionp->stats.s_shots_fired, cfp);
cfwrite_uint(missionp->stats.s_shots_hit, cfp);
cfwrite_uint(missionp->stats.s_bonehead_hits, cfp);
// ship kills (scoring)
for (j = 0; j < ship_info_size(); j++) {
cfwrite_int(missionp->stats.kills[j], cfp);
}
// medals earned (scoring)
for (j = 0; j < (int)Medals.size(); j++) {
cfwrite_int(missionp->stats.medal_counts[j], cfp);
}
}
}
endSection();
}
void pilotfile::csg_read_techroom()
{
int idx, list_size = 0;
TechroomState state;
if ( !m_have_info ) {
throw "Techroom before Info!";
}
// visible ships
list_size = (int)ship_list.size();
for (idx = 0; idx < list_size; idx++) {
state = (TechroomState) cfread_ubyte(cfp);
if (state != TechroomState::DEFAULT) {
if (ship_list[idx].index >= 0) {
if (state == TechroomState::ADDED) {
Ship_info[ship_list[idx].index].flags.set(Ship::Info_Flags::In_tech_database);
} else if (state == TechroomState::REMOVED) {
Ship_info[ship_list[idx].index].flags.remove(Ship::Info_Flags::In_tech_database);
} else {
mprintf(("Unrecognized techroom state: %d\n", (int) state));
}
} else {
mprintf(("Found invalid ship \"%s\" in campaign save file. "
"Skipping...\n",
ship_list[idx].name.c_str()));
}
}
}
// visible weapons
list_size = (int)weapon_list.size();
for (idx = 0; idx < list_size; idx++) {
state = (TechroomState) cfread_ubyte(cfp);
if (state != TechroomState::DEFAULT) {
if (weapon_list[idx].index >= 0) {
if (state == TechroomState::ADDED) {
Weapon_info[weapon_list[idx].index].wi_flags.set(Weapon::Info_Flags::In_tech_database);
} else if (state == TechroomState::REMOVED) {
Weapon_info[weapon_list[idx].index].wi_flags.remove(Weapon::Info_Flags::In_tech_database);
} else {
mprintf(("Unrecognized techroom state: %d\n", (int) state));
}
} else {
mprintf(("Found invalid weapon \"%s\" in campaign save file. Skipping...\n",
weapon_list[idx].name.c_str()));
}
}
}
// visible intel entries
list_size = (int)intel_list.size();
for (idx = 0; idx < list_size; idx++) {
state = (TechroomState) cfread_ubyte(cfp);
if (state != TechroomState::DEFAULT) {
if (intel_list[idx].index >= 0) {
if (state == TechroomState::ADDED) {
Intel_info[intel_list[idx].index].flags |= IIF_IN_TECH_DATABASE;
} else if (state == TechroomState::REMOVED) {
Intel_info[intel_list[idx].index].flags &= ~IIF_IN_TECH_DATABASE;
} else {
mprintf(("Unrecognized techroom state: %d\n", (int) state));
}
} else {
mprintf(("Found invalid intel entry \"%s\" in campaign save file. Skipping...\n",
intel_list[idx].name.c_str()));
}
}
}
// if anything we need/use was missing then it should be considered fatal
if (m_data_invalid) {
throw "Invalid data for CSG!";
}
}
void pilotfile::csg_write_techroom()
{
TechroomState state;
startSection(Section::Techroom);
// write whether it differs from the default for ships
for (auto &si : Ship_info) {
if (si.flags[Ship::Info_Flags::In_tech_database] == si.flags[Ship::Info_Flags::Default_in_tech_database]) {
state = TechroomState::DEFAULT;
} else if (si.flags[Ship::Info_Flags::In_tech_database]) {
state = TechroomState::ADDED;
} else {
state = TechroomState::REMOVED;
}
cfwrite_ubyte((ubyte) state, cfp);
}
// and for weapons
for (auto &wi : Weapon_info) {
if (wi.wi_flags[Weapon::Info_Flags::In_tech_database] == wi.wi_flags[Weapon::Info_Flags::Default_in_tech_database]) {
state = TechroomState::DEFAULT;
} else if (wi.wi_flags[Weapon::Info_Flags::In_tech_database]) {
state = TechroomState::ADDED;
} else {
state = TechroomState::REMOVED;
}
cfwrite_ubyte((ubyte) state, cfp);
}
// and for intel entries
for (auto &ii : Intel_info) {
if (((ii.flags & IIF_IN_TECH_DATABASE) != 0) == ((ii.flags & IIF_DEFAULT_IN_TECH_DATABASE) != 0)) {
state = TechroomState::DEFAULT;
} else if ((ii.flags & IIF_IN_TECH_DATABASE) != 0) {
state = TechroomState::ADDED;
} else {
state = TechroomState::REMOVED;
}
cfwrite_ubyte((ubyte) state, cfp);
}
endSection();
}
void pilotfile::csg_read_loadout()
{
int j, count, ship_idx = -1, wep_idx = -1;
size_t idx, list_size = 0;
if ( !m_have_info ) {
throw "Loadout before Info!";
}
// base info
cfread_string_len(Player_loadout.filename, MAX_FILENAME_LEN, cfp);
cfread_string_len(Player_loadout.last_modified, DATE_TIME_LENGTH, cfp);
// ship pool
list_size = ship_list.size();
for (idx = 0; idx < list_size; idx++) {
count = cfread_int(cfp);
if (ship_list[idx].index >= 0) {
Player_loadout.ship_pool[ship_list[idx].index] = count;
}
}
// weapon pool
list_size = weapon_list.size();
for (idx = 0; idx < list_size; idx++) {
count = cfread_int(cfp);
if (weapon_list[idx].index >= 0) {
Player_loadout.weapon_pool[weapon_list[idx].index] = count;
}
}
// player ship loadout
list_size = (uint)cfread_ushort(cfp);
for (uint i = 0; i < list_size; i++) {
wss_unit *slot = NULL;
if (i < MAX_WSS_SLOTS) {
slot = &Player_loadout.unit_data[i];
}
// ship
ship_idx = cfread_int(cfp);
if ( (ship_idx >= (int)ship_list.size()) || (ship_idx < -1) ) { // on the casts, assume that ship & weapon lists will never exceed ~2 billion
mprintf(("CSG => Parse Warning: Invalid value for ship index (%d), emptying slot.\n", ship_idx));
ship_idx = -1;
}
if (slot) {
if (ship_idx == -1) { // -1 means no ship in this slot
slot->ship_class = -1;
} else {
slot->ship_class = ship_list[ship_idx].index;
}
}
// primary weapons
count = cfread_int(cfp);
for (j = 0; j < count; j++) {
wep_idx = cfread_int(cfp);
if ( (wep_idx >= (int)weapon_list.size()) || (wep_idx < -1) ) {
mprintf(("CSG => Parse Warning: Invalid value for primary weapon index (%d), emptying slot.\n", wep_idx));
wep_idx = -1;
}
if ( slot && (j < MAX_SHIP_PRIMARY_BANKS) ) {
if (wep_idx == -1) { // -1 means no weapon in this slot
slot->wep[j] = -1;
} else {
slot->wep[j] = weapon_list[wep_idx].index;
}
}
int read_idx = cfread_int(cfp);
if ( slot && (j < MAX_SHIP_PRIMARY_BANKS) ) {
slot->wep_count[j] = read_idx;
}
}
// secondary weapons
count = cfread_int(cfp);
for (j = 0; j < count; j++) {
wep_idx = cfread_int(cfp);
if ( (wep_idx >= (int)weapon_list.size()) || (wep_idx < -1) ) {
mprintf(("CSG => Parse Warning: Invalid value for secondary weapon index (%d), emptying slot.\n", wep_idx));
wep_idx = -1;
}
if ( slot && (j < MAX_SHIP_SECONDARY_BANKS) ) {
if (wep_idx == -1) { // -1 means no weapon in this slot
slot->wep[j+MAX_SHIP_PRIMARY_BANKS] = -1;
} else {
slot->wep[j+MAX_SHIP_PRIMARY_BANKS] = weapon_list[wep_idx].index;
}
}
int read_idx = cfread_int(cfp);
if ( slot && (j < MAX_SHIP_SECONDARY_BANKS) ) {
slot->wep_count[j+MAX_SHIP_PRIMARY_BANKS] = read_idx;
}
}
}
}
void pilotfile::csg_write_loadout()
{
int idx, j;
startSection(Section::Loadout);
// base info
cfwrite_string_len(Player_loadout.filename, cfp);
cfwrite_string_len(Player_loadout.last_modified, cfp);
// ship pool
for (idx = 0; idx < ship_info_size(); idx++) {
cfwrite_int(Player_loadout.ship_pool[idx], cfp);
}
// weapon pool
for (idx = 0; idx < weapon_info_size(); idx++) {
cfwrite_int(Player_loadout.weapon_pool[idx], cfp);
}
// play ship loadout
cfwrite_ushort(MAX_WSS_SLOTS, cfp);
for (idx = 0; idx < MAX_WSS_SLOTS; idx++) {
wss_unit *slot = &Player_loadout.unit_data[idx];
// ship
cfwrite_int(slot->ship_class, cfp);
// primary weapons
cfwrite_int(MAX_SHIP_PRIMARY_BANKS, cfp);
for (j = 0; j < MAX_SHIP_PRIMARY_BANKS; j++) {
cfwrite_int(slot->wep[j], cfp);
cfwrite_int(slot->wep_count[j], cfp);
}
// secondary weapons
cfwrite_int(MAX_SHIP_SECONDARY_BANKS, cfp);
for (j = 0; j < MAX_SHIP_SECONDARY_BANKS; j++) {
cfwrite_int(slot->wep[j+MAX_SHIP_PRIMARY_BANKS], cfp);
cfwrite_int(slot->wep_count[j+MAX_SHIP_PRIMARY_BANKS], cfp);
}
}
endSection();
}
void pilotfile::csg_read_stats()
{
int idx, list_size = 0;
int count;
if ( !m_have_info ) {
throw "Stats before Info!";
}
// scoring stats
p->stats.score = cfread_int(cfp);
p->stats.rank = cfread_int(cfp);
p->stats.assists = cfread_int(cfp);
p->stats.kill_count = cfread_int(cfp);
p->stats.kill_count_ok = cfread_int(cfp);
p->stats.bonehead_kills = cfread_int(cfp);
p->stats.p_shots_fired = cfread_uint(cfp);
p->stats.p_shots_hit = cfread_uint(cfp);
p->stats.p_bonehead_hits = cfread_uint(cfp);
p->stats.s_shots_fired = cfread_uint(cfp);
p->stats.s_shots_hit = cfread_uint(cfp);
p->stats.s_bonehead_hits = cfread_uint(cfp);
p->stats.flight_time = cfread_uint(cfp);
p->stats.missions_flown = cfread_uint(cfp);
p->stats.last_flown = (_fs_time_t)cfread_int(cfp);
p->stats.last_backup = (_fs_time_t)cfread_int(cfp);
// ship kills (scoring)
list_size = (int)ship_list.size();
for (idx = 0; idx < list_size; idx++) {
count = cfread_int(cfp);
if (ship_list[idx].index >= 0) {
p->stats.kills[ship_list[idx].index] = count;
}
}
// medals earned (scoring)
list_size = (int)medals_list.size();
for (idx = 0; idx < list_size; idx++) {
count = cfread_int(cfp);
if (medals_list[idx].index >= 0) {
p->stats.medal_counts[medals_list[idx].index] = count;
}
}
}
void pilotfile::csg_write_stats()
{
int idx;
startSection(Section::Scoring);
// scoring stats
cfwrite_int(p->stats.score, cfp);
cfwrite_int(p->stats.rank, cfp);
cfwrite_int(p->stats.assists, cfp);
cfwrite_int(p->stats.kill_count, cfp);
cfwrite_int(p->stats.kill_count_ok, cfp);
cfwrite_int(p->stats.bonehead_kills, cfp);
cfwrite_uint(p->stats.p_shots_fired, cfp);
cfwrite_uint(p->stats.p_shots_hit, cfp);
cfwrite_uint(p->stats.p_bonehead_hits, cfp);
cfwrite_uint(p->stats.s_shots_fired, cfp);
cfwrite_uint(p->stats.s_shots_hit, cfp);
cfwrite_uint(p->stats.s_bonehead_hits, cfp);
cfwrite_uint(p->stats.flight_time, cfp);
cfwrite_uint(p->stats.missions_flown, cfp);
cfwrite_int((int)p->stats.last_flown, cfp);
cfwrite_int((int)p->stats.last_backup, cfp);
// ship kills (scoring)
for (idx = 0; idx < ship_info_size(); idx++) {
cfwrite_int(p->stats.kills[idx], cfp);
}
// medals earned (scoring)
for (idx = 0; idx < (int)Medals.size(); idx++) {
cfwrite_int(p->stats.medal_counts[idx], cfp);
}
endSection();
}
void pilotfile::csg_read_redalert()
{
int idx, i, j, ship_list_size = 0, wing_list_size = 0;
int count;
char t_string[MAX_FILENAME_LEN+NAME_LENGTH+1] = { '\0' };
float hit;
wep_t weapons;
if ( !m_have_info ) {
throw "RedAlert before Info!";
}
ship_list_size = cfread_int(cfp);
if (ship_list_size > 0) {
cfread_string_len(t_string, MAX_FILENAME_LEN, cfp);
Red_alert_precursor_mission = t_string;
for (idx = 0; idx < ship_list_size; idx++) {
red_alert_ship_status ras;
cfread_string_len(t_string, NAME_LENGTH, cfp);
ras.name = t_string;
ras.hull = cfread_float(cfp);
// ship class, index into ship_list[]
i = cfread_int(cfp);
if ( (i >= (int)ship_list.size()) || (i < RED_ALERT_LOWEST_VALID_SHIP_CLASS) ) {
mprintf(("CSG => Parse Warning: Invalid value for red alert ship index (%d), emptying slot.\n", i));
ras.ship_class = RED_ALERT_DESTROYED_SHIP_CLASS;
} else if ( (i < 0 ) && (i >= RED_ALERT_LOWEST_VALID_SHIP_CLASS) ) { // ship destroyed/exited
ras.ship_class = i;
} else {
ras.ship_class = ship_list[i].index;
}
// subsystem hits
count = cfread_int(cfp);
for (j = 0; j < count; j++) {
hit = cfread_float(cfp);
ras.subsys_current_hits.push_back( hit );
}
// subsystem aggregate hits
count = cfread_int(cfp);
for (j = 0; j < count; j++) {
hit = cfread_float(cfp);
ras.subsys_aggregate_current_hits.push_back( hit );
}
// primary weapon loadout and status
count = cfread_int(cfp);
for (j = 0; j < count; j++) {
i = cfread_int(cfp);
weapons.index = weapon_list[i].index;
weapons.count = cfread_int(cfp);
// triggering this means something is really fubar
if (weapons.index < 0) {
continue;
}
ras.primary_weapons.push_back( weapons );
}
// secondary weapon loadout and status
count = cfread_int(cfp);
for (j = 0; j < count; j++) {
i = cfread_int(cfp);
weapons.index = weapon_list[i].index;
weapons.count = cfread_int(cfp);
// triggering this means something is really fubar
if (weapons.index < 0) {
continue;
}
ras.secondary_weapons.push_back( weapons );
}
// this is quite likely a *bad* thing if it doesn't happen
if (ras.ship_class >= RED_ALERT_LOWEST_VALID_SHIP_CLASS) {
Red_alert_ship_status.push_back( std::move(ras) );
}
}
}
// old versions of CSG files do not store wing status
if (csg_ver < 8) {
return;
}
wing_list_size = cfread_int(cfp);
if (wing_list_size > 0) {
for (idx = 0; idx < wing_list_size; idx++) {
red_alert_wing_status rws;
cfread_string_len(t_string, NAME_LENGTH, cfp);
rws.name = t_string;
rws.latest_wave = cfread_int(cfp);
rws.wave_count = cfread_int(cfp);
rws.total_arrived_count = cfread_int(cfp);
rws.total_departed = cfread_int(cfp);
rws.total_destroyed = cfread_int(cfp);
rws.total_vanished = cfread_int(cfp);
Red_alert_wing_status.push_back(std::move(rws));
}
}
}
void pilotfile::csg_write_redalert()
{
int idx, j, ship_list_size = 0, wing_list_size = 0;
int count;
startSection(Section::RedAlert);
ship_list_size = (int)Red_alert_ship_status.size();
cfwrite_int(ship_list_size, cfp);
if (ship_list_size) {
cfwrite_string_len(Red_alert_precursor_mission.c_str(), cfp);
for (idx = 0; idx < ship_list_size; idx++) {
auto ras = &Red_alert_ship_status[idx];
cfwrite_string_len(ras->name.c_str(), cfp);
cfwrite_float(ras->hull, cfp);
// ship class, should be index into ship_list[] on load
cfwrite_int(ras->ship_class, cfp);
// subsystem hits
count = (int)ras->subsys_current_hits.size();
cfwrite_int(count, cfp);
for (j = 0; j < count; j++) {
cfwrite_float(ras->subsys_current_hits[j], cfp);
}
// subsystem aggregate hits
count = (int)ras->subsys_aggregate_current_hits.size();
cfwrite_int(count, cfp);
for (j = 0; j < count; j++) {
cfwrite_float(ras->subsys_aggregate_current_hits[j], cfp);
}
// primary weapon loadout and status
count = (int)ras->primary_weapons.size();
cfwrite_int(count, cfp);
for (j = 0; j < count; j++) {
cfwrite_int(ras->primary_weapons[j].index, cfp);
cfwrite_int(ras->primary_weapons[j].count, cfp);
}
// secondary weapon loadout and status
count = (int)ras->secondary_weapons.size();
cfwrite_int(count, cfp);
for (j = 0; j < count; j++) {
cfwrite_int(ras->secondary_weapons[j].index, cfp);
cfwrite_int(ras->secondary_weapons[j].count, cfp);
}
}
}
wing_list_size = (int)Red_alert_wing_status.size();
cfwrite_int(wing_list_size, cfp);
if (wing_list_size) {
for (idx = 0; idx < wing_list_size; idx++) {
auto rws = &Red_alert_wing_status[idx];
cfwrite_string_len(rws->name.c_str(), cfp);
cfwrite_int(rws->latest_wave, cfp);
cfwrite_int(rws->wave_count, cfp);
cfwrite_int(rws->total_arrived_count, cfp);
cfwrite_int(rws->total_departed, cfp);
cfwrite_int(rws->total_destroyed, cfp);
cfwrite_int(rws->total_vanished, cfp);
}
}
endSection();
}
void pilotfile::csg_read_hud()
{
const HC_gauge_mappings& gauge_map = HC_gauge_mappings::get_instance();
int strikes = 0;
// flags
int show_flags = cfread_int(cfp);
int show_flags2 = cfread_int(cfp);
int popup_flags = cfread_int(cfp);
int popup_flags2 = cfread_int(cfp);
// Convert show_flags (0-31) and show_flags2 (32-63)
for (int i = 0; i < 64; i++) {
bool is_set = (i < 32) ? (show_flags & (1 << i)) : (show_flags2 & (1 << (i - 32)));
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(i);
if (!gauge_id.empty()) {
HUD_config.set_gauge_visibility(gauge_id, is_set);
}
}
// Convert popup_flags (0-31) and popup_flags2 (32-63)
for (int i = 0; i < 64; i++) {
bool is_set = (i < 32) ? (popup_flags & (1 << i)) : (popup_flags2 & (1 << (i - 32)));
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(i);
if (!gauge_id.empty()) {
HUD_config.set_gauge_popup(gauge_id, is_set);
}
}
// settings
SCP_UNUSED(cfread_ubyte(cfp));// Deprecated but still read for file compatibility 3/7/2025
SCP_UNUSED(cfread_int(cfp));// Deprecated but still read for file compatibility 3/7/2025
HUD_config.rp_dist = cfread_int(cfp);
if (HUD_config.rp_dist < 0 || HUD_config.rp_dist >= RR_MAX_RANGES) {
ReleaseWarning(LOCATION, "Campaign file has invalid radar range %d, setting to default.\n", HUD_config.rp_dist);
HUD_config.rp_dist = RR_INFINITY;
strikes++;
}
// basic colors
HUD_config.main_color = cfread_int(cfp);
if (HUD_config.main_color < 0 || HUD_config.main_color >= NUM_HUD_COLOR_PRESETS) {
ReleaseWarning(LOCATION, "Campaign file has invalid main color selection %i, setting to default.\n", HUD_config.main_color);
HUD_config.main_color = HUD_COLOR_PRESET_1;
strikes++;
}
HUD_color_alpha = cfread_int(cfp);
if (HUD_color_alpha < HUD_COLOR_ALPHA_USER_MIN || HUD_color_alpha > HUD_COLOR_ALPHA_USER_MAX) {
ReleaseWarning(LOCATION, "Campaign file has invalid alpha color %i, setting to default.\n", HUD_color_alpha);
HUD_color_alpha = HUD_COLOR_ALPHA_DEFAULT;
strikes++;
}
if (strikes == 3) {
ReleaseWarning(LOCATION, "Campaign file has too many hud config errors, and is likely corrupted. Please verify and save your settings in the hud config menu.");
}
hud_config_record_color(HUD_config.main_color);
// gauge-specific colors
int num_gauges = cfread_int(cfp);
for (int idx = 0; idx < num_gauges; idx++) {
ubyte red = cfread_ubyte(cfp);
ubyte green = cfread_ubyte(cfp);
ubyte blue = cfread_ubyte(cfp);
ubyte alpha = cfread_ubyte(cfp);
if (idx >= NUM_HUD_GAUGES) {
continue;
}
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(idx);
if (!gauge_id.empty()) {
color clr;
gr_init_alphacolor(&clr, red, green, blue, alpha);
HUD_config.set_gauge_color(gauge_id, clr);
}
}
}
void pilotfile::csg_write_hud()
{
startSection(Section::HUD);
// Get gauge mappings instance
const HC_gauge_mappings& gauge_map = HC_gauge_mappings::get_instance();
// Initialize bitfields
int show_flags = 0, show_flags2 = 0;
int popup_flags = 0, popup_flags2 = 0;
// Convert show_flags_map to bitfield
for (int i = 0; i < 64; i++) {
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(i);
if (!gauge_id.empty() && HUD_config.is_gauge_visible(gauge_id)) {
if (i < 32) {
show_flags |= (1 << i);
} else {
show_flags2 |= (1 << (i - 32));
}
}
}
// Convert popup_flags_map to bitfield
for (int i = 0; i < 64; i++) {
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(i);
if (!gauge_id.empty() && HUD_config.is_gauge_popup(gauge_id)) {
if (i < 32) {
popup_flags |= (1 << i);
} else {
popup_flags2 |= (1 << (i - 32));
}
}
}
// flags
cfwrite_int(show_flags, cfp);
cfwrite_int(show_flags2, cfp);
cfwrite_int(popup_flags, cfp);
cfwrite_int(popup_flags2, cfp);
// settings
cfwrite_ubyte(0, cfp);// Deprecated but still written for file compatibility 3/7/2025
cfwrite_int(0, cfp);// Deprecated but still written for file compatibility 3/7/2025
cfwrite_int(HUD_config.rp_dist, cfp);
// basic colors
cfwrite_int(HUD_config.main_color, cfp);
cfwrite_int(HUD_color_alpha, cfp);
// gauge-specific colors
cfwrite_int(NUM_HUD_GAUGES, cfp);
for (int idx = 0; idx < NUM_HUD_GAUGES; idx++) {
// Get the gauge string ID from numeric ID
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(idx);
color clr = HUD_config.get_gauge_color(gauge_id);
cfwrite_ubyte(clr.red, cfp);
cfwrite_ubyte(clr.green, cfp);
cfwrite_ubyte(clr.blue, cfp);
cfwrite_ubyte(clr.alpha, cfp);
}
endSection();
}
void pilotfile::csg_read_variables()
{
int idx;
int num_variables = cfread_int(cfp);
if (num_variables > 0) {
for (idx = 0; idx < num_variables; idx++) {
sexp_variable temp_var;
memset(&temp_var, 0, sizeof(sexp_variable));
temp_var.type = cfread_int(cfp);
cfread_string_len(temp_var.text, TOKEN_LENGTH, cfp);
cfread_string_len(temp_var.variable_name, TOKEN_LENGTH, cfp);
Campaign.persistent_variables.push_back(std::move(temp_var));
// eternal variables belong in the player file
Assert(!(temp_var.type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE));
if (temp_var.type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) {
Campaign.persistent_variables.pop_back();
}
}
}
Campaign.red_alert_variables.clear();
if (csg_ver < 4) { // CSG files before version 4 don't have a Red Alert set of CPVs to load, so just copy the regular set.
for (auto& current_pv : Campaign.persistent_variables) {
Campaign.red_alert_variables.push_back(current_pv);
}
} else {
int redalert_num_variables = cfread_int(cfp);
if (redalert_num_variables > 0) {
for (idx = 0; idx < redalert_num_variables; idx++) {
sexp_variable temp_var;
memset(&temp_var, 0, sizeof(sexp_variable));
temp_var.type = cfread_int(cfp);
cfread_string_len(temp_var.text, TOKEN_LENGTH, cfp);
cfread_string_len(temp_var.variable_name, TOKEN_LENGTH, cfp);
Campaign.red_alert_variables.push_back(std::move(temp_var));
}
}
}
}
void pilotfile::csg_write_variables()
{
int idx;
startSection(Section::Variables);
cfwrite_int((int)Campaign.persistent_variables.size(), cfp);
for (idx = 0; idx < (int)Campaign.persistent_variables.size(); idx++) {
cfwrite_int(Campaign.persistent_variables[idx].type, cfp);
cfwrite_string_len(Campaign.persistent_variables[idx].text, cfp);
cfwrite_string_len(Campaign.persistent_variables[idx].variable_name, cfp);
}
cfwrite_int((int)Campaign.red_alert_variables.size(), cfp);
for (idx = 0; idx < (int)Campaign.red_alert_variables.size(); idx++) {
cfwrite_int(Campaign.red_alert_variables[idx].type, cfp);
cfwrite_string_len(Campaign.red_alert_variables[idx].text, cfp);
cfwrite_string_len(Campaign.red_alert_variables[idx].variable_name, cfp);
}
endSection();
}
void pilotfile::csg_read_settings()
{
clamped_range_warnings.clear();
// sound/voice/music
float temp_volume = cfread_float(cfp);
clamp_value_with_warn(&temp_volume, 0.f, 1.f, "Effects Volume");
snd_set_effects_volume(temp_volume);
options::OptionsManager::instance()->set_ingame_range_option("Audio.Effects", Master_sound_volume);
temp_volume = cfread_float(cfp);
clamp_value_with_warn(&temp_volume, 0.f, 1.f, "Music Volume");
event_music_set_volume(temp_volume);
options::OptionsManager::instance()->set_ingame_range_option("Audio.Music", Master_event_music_volume);
temp_volume = cfread_float(cfp);
clamp_value_with_warn(&temp_volume, 0.f, 1.f, "Voice Volume");
snd_set_voice_volume(temp_volume);
options::OptionsManager::instance()->set_ingame_range_option("Audio.Voice", Master_voice_volume);
Briefing_voice_enabled = cfread_int(cfp) != 0;
options::OptionsManager::instance()->set_ingame_binary_option("Audio.BriefingVoice", Briefing_voice_enabled);
// skill level
Game_skill_level = cfread_int(cfp);
clamp_value_with_warn(&Game_skill_level, 0, 4, "Game Skill Level");
options::OptionsManager::instance()->set_ingame_range_option("Game.SkillLevel", Game_skill_level);
// input options
Use_mouse_to_fly = cfread_int(cfp) != 0;
options::OptionsManager::instance()->set_ingame_binary_option("Input.UseMouse", Use_mouse_to_fly);
Mouse_sensitivity = cfread_int(cfp);
clamp_value_with_warn(&Mouse_sensitivity, 0, 9, "Mouse Sensitivity");
options::OptionsManager::instance()->set_ingame_range_option("Input.MouseSensitivity", Mouse_sensitivity);
Joy_sensitivity = cfread_int(cfp);
clamp_value_with_warn(&Joy_sensitivity, 0, 9, "Joystick Sensitivity");
options::OptionsManager::instance()->set_ingame_range_option("Input.JoystickSensitivity", Joy_sensitivity);
Joy_dead_zone_size = cfread_int(cfp);
clamp_value_with_warn(&Joy_dead_zone_size, 0, 45, "Joystick Deadzone");
options::OptionsManager::instance()->set_ingame_range_option("Input.JoystickDeadZone", Joy_dead_zone_size);
if (csg_ver < 3) {
// detail
int dummy __UNUSED = cfread_int(cfp);
dummy = cfread_int(cfp);
dummy = cfread_int(cfp);
dummy = cfread_int(cfp);
dummy = cfread_int(cfp);
dummy = cfread_int(cfp);
dummy = cfread_int(cfp);
dummy = cfread_int(cfp);
dummy = cfread_int(cfp);
dummy = cfread_int(cfp);
dummy = cfread_int(cfp);
dummy = cfread_int(cfp);
}
if (!clamped_range_warnings.empty()) {
ReleaseWarning(LOCATION, "The following values in the campaign save file were out of bounds and were automatically reset:\n%s\nPlease check your settings!\n", clamped_range_warnings.c_str());
clamped_range_warnings.clear();
}
}
void pilotfile::csg_write_settings()
{
startSection(Section::Settings);
clamped_range_warnings.clear();
// sound/voice/music
clamp_value_with_warn(&Master_sound_volume, 0.f, 1.f, "Effects Volume");
cfwrite_float(Master_sound_volume, cfp);
clamp_value_with_warn(&Master_event_music_volume, 0.f, 1.f, "Music Volume");
cfwrite_float(Master_event_music_volume, cfp);
clamp_value_with_warn(&Master_voice_volume, 0.f, 1.f, "Voice Volume");
cfwrite_float(Master_voice_volume, cfp);
cfwrite_int(Briefing_voice_enabled ? 1 : 0, cfp);
// skill level
clamp_value_with_warn(&Game_skill_level, 0, 4, "Game Skill Level");
cfwrite_int(Game_skill_level, cfp);
// input options
cfwrite_int(Use_mouse_to_fly, cfp);
clamp_value_with_warn(&Mouse_sensitivity, 0, 9, "Mouse Sensitivity");
cfwrite_int(Mouse_sensitivity, cfp);
clamp_value_with_warn(&Joy_sensitivity, 0, 9, "Joystick Sensitivity");
cfwrite_int(Joy_sensitivity, cfp);
clamp_value_with_warn(&Joy_dead_zone_size, 0, 45, "Joystick Deadzone");
cfwrite_int(Joy_dead_zone_size, cfp);
if (!clamped_range_warnings.empty()) {
ReleaseWarning(LOCATION, "The following values were out of bounds when saving the campaign file and were automatically reset.\n%s\nThis shouldn't be possible, please contact the FreeSpace 2 Open Source Code Project!\n", clamped_range_warnings.c_str());
clamped_range_warnings.clear();
}
endSection();
}
void pilotfile::csg_read_controls()
{
if (csg_ver < 7) {
// Pre CSG-7 compatibility
int idx, list_size;
short id1, id2, id3 __UNUSED;
list_size = (int)cfread_ushort(cfp);
for (idx = 0; idx < list_size; idx++) {
id1 = cfread_short(cfp);
id2 = cfread_short(cfp);
id3 = cfread_short(cfp); // unused, at the moment
if (idx < CCFG_MAX) {
Control_config[idx].take(CC_bind(CID_KEYBOARD, id1), 0);
Control_config[idx].take(CC_bind(CID_JOY0, id2), 1);
}
}
// Check that these bindings are in a preset.
// CSG doesn't save invert flags, so use agnostic equals
auto it = control_config_get_current_preset(true);
if (it == Control_config_presets.end()) {
// Not a preset, create one and its file
CC_preset preset;
preset.name = filename;
// strip off extension
auto n = preset.name.find_last_of('.');
preset.name.resize(n);
std::copy(Control_config.begin(), Control_config.end(), std::back_inserter(preset.bindings));
Control_config_presets.push_back(preset);
save_preset_file(preset, true);
}
return;
} else {
// >= CSG-7
char buf[MAX_FILENAME_LEN];
memset(buf, '\0', sizeof(buf));
cfread_string(buf, sizeof(buf), cfp);
auto it = std::find_if(Control_config_presets.begin(), Control_config_presets.end(),
[&buf](const CC_preset& preset) { return preset.name == buf; });
if (it == Control_config_presets.end()) {
Assertion(!Control_config_presets.empty(), "[CSG] Error reading CSG! Control_config_presets empty; Get a coder!");
// Couldn't find the preset, use defaults
it = Control_config_presets.begin();
}
control_config_use_preset(*it);
return;
}
}
void pilotfile::csg_write_controls()
{
auto it = control_config_get_current_preset();
if (it == Control_config_presets.end()) {
// Normally shouldn't happen, may be a new, blank player
// First assert that the presets have been initialized and have at least the defaults preset
Assertion(!Control_config_presets.empty(), "[CSG] Error saving CSG! Control_config_presets empty; Get a coder!");
// Next, just bash the current preset to be defaults
it = Control_config_presets.begin();
}
startSection(Section::Controls);
cfwrite_string(it->name.c_str(), cfp);
endSection();
}
void pilotfile::csg_read_cutscenes() {
size_t list_size = cfread_uint(cfp);
for(size_t i = 0; i < list_size; i++) {
char tempFilename[MAX_FILENAME_LEN];
cfread_string_len(tempFilename, MAX_FILENAME_LEN, cfp);
cutscene_mark_viewable(tempFilename);
}
}
void pilotfile::csg_write_cutscenes() {
SCP_vector<cutscene_info>::iterator cut;
startSection(Section::Cutscenes);
size_t viewableScenes = 0;
for (cut = Cutscenes.begin(); cut != Cutscenes.end(); ++cut) {
if (cut->flags[Cutscene::Cutscene_Flags::Viewable]) {
viewableScenes++;
}
}
// Check for possible overflow because we can only write 32 bit integers
Assertion(viewableScenes <= std::numeric_limits<uint>::max(), "Too many viewable cutscenes! Maximum is %ud!", std::numeric_limits<uint>::max());
cfwrite_uint((uint)viewableScenes, cfp);
for (cut = Cutscenes.begin(); cut != Cutscenes.end(); ++cut) {
if (cut->flags[Cutscene::Cutscene_Flags::Viewable]) {
cfwrite_string_len(cut->filename, cfp);
}
}
endSection();
}
/*
* Only used for quick start missions
*/
void pilotfile::csg_read_lastmissions()
{
int i;
// restore list of most recently played missions
Num_recent_missions = cfread_int( cfp );
Assert(Num_recent_missions <= MAX_RECENT_MISSIONS);
for ( i = 0; i < Num_recent_missions; i++ ) {
char *cp;
cfread_string_len( Recent_missions[i], MAX_FILENAME_LEN, cfp);
// Remove the extension (safety check: shouldn't exist anyway)
cp = strchr(Recent_missions[i], '.');
if (cp)
*cp = 0;
}
}
/*
* Only used for quick start missions
*/
void pilotfile::csg_write_lastmissions()
{
int i;
startSection(Section::LastMissions);
// store list of most recently played missions
cfwrite_int(Num_recent_missions, cfp);
for (i=0; i<Num_recent_missions; i++) {
cfwrite_string_len(Recent_missions[i], cfp);
}
endSection();
}
void pilotfile::csg_read_containers()
{
const int num_containers = cfread_int(cfp);
for (int idx = 0; idx < num_containers; idx++) {
Campaign.persistent_containers.emplace_back();
auto& container = Campaign.persistent_containers.back();
csg_read_container(container);
// eternal containers belong in the player file
Assert(!container.is_eternal());
if (container.is_eternal()) {
Campaign.persistent_containers.pop_back();
}
}
Campaign.red_alert_containers.clear();
const int redalert_num_containers = cfread_int(cfp);
for (int idx = 0; idx < redalert_num_containers; idx++) {
Campaign.red_alert_containers.emplace_back();
auto& ra_container = Campaign.red_alert_containers.back();
csg_read_container(ra_container);
}
}
void pilotfile::csg_read_container(sexp_container& container)
{
char temp_buf[NAME_LENGTH];
memset(temp_buf, 0, sizeof(temp_buf));
cfread_string_len(temp_buf, sizeof(temp_buf), cfp);
container.container_name = temp_buf;
container.type = (ContainerType)cfread_int(cfp);
container.opf_type = cfread_int(cfp);
const int size = cfread_int(cfp);
if (container.is_list()) {
for (int i = 0; i < size; ++i) {
cfread_string_len(temp_buf, sizeof(temp_buf), cfp);
container.list_data.emplace_back(temp_buf);
}
} else if (container.is_map()) {
char temp_key[NAME_LENGTH];
memset(temp_key, 0, sizeof(temp_key));
for (int i = 0; i < size; ++i) {
cfread_string_len(temp_key, sizeof(temp_key), cfp);
cfread_string_len(temp_buf, sizeof(temp_buf), cfp);
container.map_data.emplace(temp_key, temp_buf);
}
} else {
UNREACHABLE("Unknown container type %d", static_cast<int>(container.type));
}
}
void pilotfile::csg_write_containers()
{
startSection(Section::Containers);
cfwrite_int((int)Campaign.persistent_containers.size(), cfp);
for (const auto& container : Campaign.persistent_containers) {
csg_write_container(container);
}
cfwrite_int((int)Campaign.red_alert_containers.size(), cfp);
for (const auto& ra_container : Campaign.red_alert_containers) {
csg_write_container(ra_container);
}
endSection();
}
void pilotfile::csg_write_container(const sexp_container &container)
{
cfwrite_string_len(container.container_name.c_str(), cfp);
cfwrite_int((int)container.type, cfp);
cfwrite_int(container.opf_type, cfp);
cfwrite_int(container.size(), cfp);
if (container.is_list()) {
for (const auto& data : container.list_data) {
cfwrite_string_len(data.c_str(), cfp);
}
} else if (container.is_map()) {
for (const auto& key_data : container.map_data) {
cfwrite_string_len(key_data.first.c_str(), cfp);
cfwrite_string_len(key_data.second.c_str(), cfp);
}
} else {
UNREACHABLE("Unknown container type %d", static_cast<int>(container.type));
}
}
void pilotfile::csg_reset_data(bool reset_ships_and_weapons)
{
int idx;
cmission *missionp;
// internals
m_have_flags = false;
m_have_info = false;
m_data_invalid = false;
// init stats
p->stats.init();
// zero out allowed ships/weapons
if (reset_ships_and_weapons) {
memset(Campaign.ships_allowed, 0, sizeof(Campaign.ships_allowed));
memset(Campaign.weapons_allowed, 0, sizeof(Campaign.weapons_allowed));
}
// reset campaign status
Campaign.prev_mission = -1;
Campaign.next_mission = 0;
Campaign.num_missions_completed = 0;
// techroom reset
tech_reset_to_default();
// clear out variables
Campaign.persistent_variables.clear();
Campaign.red_alert_variables.clear();
// clear out containers
Campaign.persistent_containers.clear();
Campaign.red_alert_containers.clear();
// clear red alert data
Red_alert_ship_status.clear();
Red_alert_wing_status.clear();
// clear out mission stuff
for (idx = 0; idx < MAX_CAMPAIGN_MISSIONS; idx++) {
missionp = &Campaign.missions[idx];
missionp->goals.clear();
missionp->events.clear();
missionp->variables.clear();
missionp->stats.init();
}
}
void pilotfile::csg_close()
{
if (cfp) {
cfclose(cfp);
cfp = NULL;
}
p = NULL;
filename = "";
ship_list.clear();
weapon_list.clear();
intel_list.clear();
medals_list.clear();
m_have_flags = false;
m_have_info = false;
csg_ver = PLR_VERSION_INVALID;
}
bool pilotfile::load_savefile(player *_p, const char *campaign)
{
SCP_string campaign_filename;
if (Game_mode & GM_MULTIPLAYER) {
return false;
}
if ( (campaign == NULL) || !strlen(campaign) ) {
return false;
}
// set player ptr first thing
Assert( (Player_num >= 0) && (Player_num < MAX_PLAYERS) );
p = _p;
auto base = util::get_file_part(campaign);
// do a sanity check, but don't arbitrarily drop any extension in case the filename contains a period
Assertion(!stristr(base, FS_CAMPAIGN_FILE_EXT), "The campaign should not have an extension at this point!");
// build up filename for the savefile...
sprintf(filename, NOX("%s.%s.csg"), p->callsign, base);
// if campaign file doesn't exist, abort so we don't load irrelevant data
campaign_filename = base;
campaign_filename += FS_CAMPAIGN_FILE_EXT;
if ( !cf_exists_full(campaign_filename.c_str(), CF_TYPE_MISSIONS) ) {
mprintf(("CSG => Unable to find campaign file '%s'!\n", campaign_filename.c_str()));
return false;
}
// we need to reset this early, in case open fails and we need to create
m_data_invalid = false;
// open it, hopefully...
cfp = cfopen(filename.c_str(), "rb", CF_TYPE_PLAYERS, false,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
if ( !cfp ) {
mprintf(("CSG => Unable to open '%s' for reading!\n", filename.c_str()));
return false;
}
unsigned int csg_id = cfread_uint(cfp);
if (csg_id != CSG_FILE_ID) {
mprintf(("CSG => Invalid header id for '%s'!\n", filename.c_str()));
csg_close();
return false;
}
// version, now used
csg_ver = cfread_ubyte(cfp);
mprintf(("CSG => Loading '%s' with version %d...\n", filename.c_str(), (int)csg_ver));
csg_reset_data(true);
// the point of all this: read in the CSG contents
while ( !cfeof(cfp) ) {
Section section_id = static_cast<Section>(cfread_ushort(cfp));
uint section_size = cfread_uint(cfp);
size_t start_pos = cftell(cfp);
// safety, to help protect against long reads
cf_set_max_read_len(cfp, section_size);
try {
switch (section_id) {
case Section::Flags:
mprintf(("CSG => Parsing: Flags...\n"));
m_have_flags = true;
csg_read_flags();
break;
case Section::Info:
mprintf(("CSG => Parsing: Info...\n"));
m_have_info = true;
csg_read_info();
break;
case Section::Variables:
mprintf(("CSG => Parsing: Variables...\n"));
csg_read_variables();
break;
case Section::HUD:
mprintf(("CSG => Parsing: HUD...\n"));
csg_read_hud();
break;
case Section::RedAlert:
mprintf(("CSG => Parsing: RedAlert...\n"));
csg_read_redalert();
break;
case Section::Scoring:
mprintf(("CSG => Parsing: Scoring...\n"));
csg_read_stats();
break;
case Section::Loadout:
mprintf(("CSG => Parsing: Loadout...\n"));
csg_read_loadout();
break;
case Section::Techroom:
mprintf(("CSG => Parsing: Techroom...\n"));
csg_read_techroom();
break;
case Section::Missions:
mprintf(("CSG => Parsing: Missions...\n"));
csg_read_missions();
break;
case Section::Settings:
mprintf(("CSG => Parsing: Settings...\n"));
csg_read_settings();
break;
case Section::Controls:
mprintf(("CSG => Parsing: Controls...\n"));
csg_read_controls();
break;
case Section::Cutscenes:
mprintf(("CSG => Parsing: Cutscenes...\n"));
csg_read_cutscenes();
break;
case Section::LastMissions:
mprintf(("CSG => Parsing: Last Missions...\n"));
csg_read_lastmissions();
break;
case Section::Containers:
mprintf(("CSG => Parsing: Containers...\n"));
csg_read_containers();
break;
default:
mprintf(("CSG => Skipping unknown section 0x%04x!\n", (uint32_t)section_id));
break;
}
} catch (cfile::max_read_length &msg) {
// read to max section size, move to next section, discarding
// extra/unknown data
mprintf(("CSG => Warning: (0x%04x) %s\n", (uint32_t)section_id, msg.what()));
} catch (const char *err) {
mprintf(("CSG => ERROR: %s\n", err));
csg_close();
return false;
}
// reset safety catch
cf_set_max_read_len(cfp, 0);
// skip to next section (if not already there)
size_t offset_pos = (start_pos + section_size) - cftell(cfp);
if (offset_pos) {
mprintf(("CSG => Warning: (0x%04x) Short read, information may have been lost!\n", (uint32_t)section_id));
cfseek(cfp, (int)offset_pos, CF_SEEK_CUR);
}
}
mprintf(("HUDPREFS => Loading extended player HUD preferences...\n"));
hud_config_load_player_prefs(p->callsign);
// Probably don't need to persist these to disk but it'll make sure on next boot we start with these campaign options set
// The github tests don't know what to do with the ini file so I guess we'll skip this for now
//options::OptionsManager::instance()->persistChanges();
// if the campaign (for whatever reason) doesn't have a squad image, use the multi one
if (p->s_squad_filename[0] == '\0') {
strcpy_s(p->s_squad_filename, p->m_squad_filename);
}
player_set_squad_bitmap(p, p->s_squad_filename, false);
mprintf(("CSG => Loading complete!\n"));
// cleanup and return
csg_close();
return true;
}
bool pilotfile::save_savefile()
{
if (Game_mode & GM_MULTIPLAYER) {
return false;
}
// set player ptr first thing
Assert( (Player_num >= 0) && (Player_num < MAX_PLAYERS) );
p = &Players[Player_num];
if ( !strlen(Campaign.filename) ) {
return false;
}
auto base = util::get_file_part(Campaign.filename);
// do a sanity check, but don't arbitrarily drop any extension in case the filename contains a period
Assertion(!stristr(base, FS_CAMPAIGN_FILE_EXT), "The campaign should not have an extension at this point!");
// build up filename for the savefile...
sprintf(filename, NOX("%s.%s.csg"), p->callsign, base);
// make sure that we can actually save this safely
if (m_data_invalid) {
mprintf(("CSG => Skipping save of '%s' due to invalid data check!\n", filename.c_str()));
return false;
}
// validate the number of red alert entries
// assertion before writing so that we don't corrupt the .csg by asserting halfway through writing
// assertion should also prevent loss of major campaign progress
// i.e. lose one mission, not several missions worth (in theory)
Assertion(Red_alert_ship_status.size() <= MAX_SHIPS, "Invalid number of Red_alert_ship_status entries: " SIZE_T_ARG "\n", Red_alert_ship_status.size());
Assertion(Red_alert_wing_status.size() <= MAX_WINGS, "Invalid number of Red_alert_wing_status entries: " SIZE_T_ARG "\n", Red_alert_wing_status.size());
// open it, hopefully...
cfp = cfopen(filename.c_str(), "wb", CF_TYPE_PLAYERS, false,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
if ( !cfp ) {
mprintf(("CSG => Unable to open '%s' for saving!\n", filename.c_str()));
return false;
}
// header and version
cfwrite_int(CSG_FILE_ID, cfp);
cfwrite_ubyte(CSG_VERSION, cfp);
mprintf(("CSG => Saving '%s' with version %d...\n", filename.c_str(), (int)CSG_VERSION));
// flags and info sections go first
mprintf(("CSG => Saving: Flags...\n"));
csg_write_flags();
mprintf(("CSG => Saving: Info...\n"));
csg_write_info();
// everything else is next, not order specific
mprintf(("CSG => Saving: Missions...\n"));
csg_write_missions();
mprintf(("CSG => Saving: Techroom...\n"));
csg_write_techroom();
mprintf(("CSG => Saving: Loadout...\n"));
csg_write_loadout();
mprintf(("CSG => Saving: Scoring...\n"));
csg_write_stats();
mprintf(("CSG => Saving: RedAlert...\n"));
csg_write_redalert();
mprintf(("CSG => Saving: HUD...\n"));
csg_write_hud();
mprintf(("CSG => Saving: Variables...\n"));
csg_write_variables();
mprintf(("CSG => Saving: Settings...\n"));
csg_write_settings();
mprintf(("CSG => Saving: Controls...\n"));
csg_write_controls();
mprintf(("CSG => Saving: Cutscenes...\n"));
csg_write_cutscenes();
mprintf(("CSG => Saving: Last Missions...\n"));
csg_write_lastmissions();
mprintf(("CSG => Saving: Containers...\n"));
csg_write_containers();
mprintf(("HUDPREFS => Saving player HUD preferences (testing)...\n"));
hud_config_save_player_prefs(p->callsign);
// Done!
mprintf(("CSG => Saving complete!\n"));
csg_close();
return true;
}
void pilotfile::clear_savefile(bool reset_ships_and_weapons)
{
if (Game_mode & GM_MULTIPLAYER) {
return;
}
// set player ptr first thing
Assert((Player_num >= 0) && (Player_num < MAX_PLAYERS));
p = &Players[Player_num];
csg_reset_data(reset_ships_and_weapons);
}
/*
* get_csg_rank: this function is called from plr.cpp & is
* tightly linked with pilotfile::verify()
*/
bool pilotfile::get_csg_rank(int *rank)
{
player t_csg;
// set player ptr first thing
p = &t_csg;
// filename has already been set
cfp = cfopen(filename.c_str(), "rb", CF_TYPE_PLAYERS, false,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
if ( !cfp ) {
mprintf(("CSG => Unable to open '%s'!\n", filename.c_str()));
return false;
}
unsigned int csg_id = cfread_uint(cfp);
if (csg_id != CSG_FILE_ID) {
mprintf(("CSG => Invalid header id for '%s'!\n", filename.c_str()));
csg_close();
return false;
}
// version, now used
csg_ver = cfread_ubyte(cfp);
mprintf(("CSG => Get Rank from '%s' with version %d...\n", filename.c_str(), (int)csg_ver));
// the point of all this: read in the CSG contents
while ( !m_have_flags && !cfeof(cfp) ) {
Section section_id = static_cast<Section>(cfread_ushort(cfp));
uint section_size = cfread_uint(cfp);
size_t start_pos = cftell(cfp);
size_t offset_pos;
// safety, to help protect against long reads
cf_set_max_read_len(cfp, section_size);
try {
switch (section_id) {
case Section::Flags:
mprintf(("CSG => Parsing: Flags...\n"));
m_have_flags = true;
csg_read_flags();
break;
default:
break;
}
} catch (cfile::max_read_length &msg) {
// read to max section size, move to next section, discarding
// extra/unknown data
mprintf(("CSG => (0x%04x) %s\n", (uint32_t)section_id, msg.what()));
} catch (const char *err) {
mprintf(("CSG => ERROR: %s\n", err));
csg_close();
return false;
}
// reset safety catch
cf_set_max_read_len(cfp, 0);
// skip to next section (if not already there)
offset_pos = (start_pos + section_size) - cftell(cfp);
if (offset_pos) {
mprintf(("CSG => Warning: (0x%04x) Short read, information may have been lost!\n", (uint32_t)section_id));
cfseek(cfp, (int)offset_pos, CF_SEEK_CUR);
}
}
// this is what we came for...
*rank = p->stats.rank;
mprintf(("CSG => Get Rank complete!\n"));
// cleanup & return
csg_close();
return true;
}